How to Deploy a Puppeteer Screenshot Script to Google Cloud Functions
Deploy a Node.js Puppeteer screenshot function with browser packaging, HTTP handling, resource settings, output choices, and fixes for common deployment errors.
To deploy a Puppeteer screenshot script, package it as a Node.js HTTP function, install puppeteer with your function dependencies, configure Puppeteer’s browser cache for the build, and deploy with an explicit entry point, region, trigger, memory, and timeout. This guide targets a second-generation Cloud Run function deployed with the current gcloud functions deploy workflow. Google’s documentation now uses the Cloud Run functions name; confirm the selected generation and supported Node.js runtime in the deployment documentation and runtime support table before publishing or deploying.
The example accepts a URL, captures a PNG, and returns it directly in the HTTP response. It is intended to demonstrate deployment. A public function that accepts arbitrary URLs needs URL allowlisting, authentication, rate limits, and an SSRF policy before production use.
1. Create the function project
Use a current Node.js runtime supported by your function generation. The supported runtime list changes, so do not treat any runtime version or lifecycle date as permanent. This example uses the Functions Framework HTTP handler and Puppeteer’s full package, which downloads a compatible Chrome for Testing browser during installation.
mkdir puppeteer-shot
cd puppeteer-shot
npm init -y
npm install @google-cloud/functions-framework puppeteer
Use puppeteer-core instead only if you manage the browser binary yourself and provide an executable path or supported browser connection. It does not download Chrome.
Create these files in the project root:
.
├── index.js
├── package.json
└── .puppeteerrc.js
Set the package start script and module type in package.json. Keep the dependency versions and lockfile generated for your project; review and commit package-lock.json so deployments resolve repeatable dependency versions.
{
"name": "puppeteer-shot",
"version": "1.0.0",
"private": true,
"main": "index.js",
"scripts": {
"start": "functions-framework --target=screenshot --signature-type=http"
},
"dependencies": {
"@google-cloud/functions-framework": "^3.0.0",
"puppeteer": "^24.0.0"
}
}
Those version ranges are illustrative. Choose versions compatible with your Node.js runtime and update policy, then preserve the resolved versions in the lockfile.
Puppeteer documents putting its downloaded browser cache under node_modules for Google Cloud Functions. Add .puppeteerrc.js at the project root:
const path = require('node:path');
module.exports = {
cacheDirectory: path.join(__dirname, 'node_modules', '.puppeteer_cache'),
};
This location addresses build cases where a cached node_modules directory means Puppeteer’s install step does not run again. Confirm that your selected build pipeline installs or preserves the browser files; the cache setting alone cannot restore a browser that the build omitted.
2. Implement the HTTP screenshot handler
The handler validates a minimal URL shape, launches Chrome, navigates, takes the screenshot, and closes the browser in a finally block. It returns image bytes with the correct content type. The navigation wait condition and timeout are workload choices: pages with long polling, animations, or delayed content may need a different readiness strategy.
const puppeteer = require('puppeteer');
exports.screenshot = async (req, res) => {
if (req.method !== 'GET') {
res.set('Allow', 'GET');
return res.status(405).send('Use GET');
}
let target;
try {
target = new URL(req.query.url);
} catch {
return res.status(400).send('Provide a valid url query parameter');
}
if (!['http:', 'https:'].includes(target.protocol)) {
return res.status(400).send('Only http and https URLs are supported');
}
let browser;
try {
browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.goto(target.href, {
waitUntil: 'networkidle2',
timeout: 45000,
});
const image = await page.screenshot({
type: 'png',
fullPage: true,
});
res.set('Content-Type', 'image/png');
res.set('Cache-Control', 'no-store');
return res.status(200).send(image);
} catch (error) {
console.error('Screenshot request failed', error);
return res.status(500).send('Screenshot capture failed');
} finally {
if (browser) {
await browser.close().catch((error) => {
console.error('Browser close failed', error);
});
}
}
};
networkidle2 waits for network activity to settle according to Puppeteer’s navigation behavior; it is not appropriate for every site. A continuously polling page may never become idle. Consider domcontentloaded, a targeted page.waitForSelector(), or a bounded delay when those match the page’s content requirements. Avoid adding launch flags copied from unrelated environments without checking whether the runtime needs them.
Choose the output contract
- Return image bytes: convenient for small synchronous results and callers that can consume binary HTTP responses. Set a content type and ensure your caller and function response limits suit the image size.
- Persist and return a reference: better for larger screenshots, asynchronous work, or later retrieval. Add a storage destination, access controls, retention policy, and error handling. The title does not prescribe a storage service.
- Return JSON with metadata: if callers need status details, encode the image or return a reference along with dimensions and capture metadata. Base64 increases payload size, so use it only when appropriate.
3. Run it locally
Start the HTTP function locally and pass a URL. The first Puppeteer installation downloads Chrome, so allow for that during setup.
npm start
curl --output shot.png "http://localhost:8080/?url=https%3A%2F%2Fexample.com"
Check that the response is a PNG and that the page content is captured at the intended viewport and scroll depth before deployment.
4. Deploy to Google Cloud
Authenticate with Google Cloud CLI, select a project, and enable the functions deployment workflow if your project requires it. Choose a region close to the target workload or caller, but account for network access and any data-location requirements.
Deploy an HTTP-triggered function with a matching entry point. Substitute a currently supported runtime and your chosen region:
gcloud functions deploy screenshot \
--gen2 \
--runtime=YOUR_SUPPORTED_NODE_RUNTIME \
--region=YOUR_REGION \
--source=. \
--entry-point=screenshot \
--trigger-http \
--memory=1GiB \
--timeout=120s
For a public endpoint, Google Cloud’s deployment command may allow unauthenticated invocation depending on flags and project policy. Do not make an arbitrary-URL screenshot endpoint public without abuse controls. Prefer authenticated invocation or put a validating, rate-limited service in front of it. Review the current gcloud functions deploy reference for generation-specific flags, trigger configuration, and access settings.
The CLI reference documents a 60-second default timeout for a new function and a 540-second maximum for first-generation functions. These values do not imply that a screenshot workload should use either setting. Browser startup, navigation, fonts, and image loading all consume time; select a limit based on representative pages and the selected generation.
5. Configure capture behavior
Most capture choices belong in the handler and should be selected for the target pages:
- Viewport and device scale: call
page.setViewport()before navigation to control dimensions and device scale factor. - Full page or viewport:
fullPage: truecaptures the page’s full scrollable area; omit it for viewport-only captures. Very tall pages can produce large images and high memory use. - Format:
page.screenshot({ type: 'png' })gives lossless PNG output. Puppeteer also supports JPEG with quality settings; verify format options against the installed Puppeteer version. - Element capture: locate the target with
page.locator(selector).screenshot()or an element handle screenshot after confirming the selector exists. - Readiness: choose a navigation wait condition, selector wait, or bounded delay that matches when the target content is actually ready.
- Authentication and page state: set cookies, headers, or form state through Puppeteer before capture when authorized. Do not put secrets in query parameters or logs.
For production input validation, resolve hostnames and reject loopback, private, link-local, and cloud metadata destinations; account for redirects and DNS changes as well. A simple scheme check is not an SSRF defense. Restrict accepted hosts whenever the use case permits.
6. Resource, performance, reliability, and cost notes
Memory and concurrency
There is no universal memory minimum for Puppeteer in the cited deployment guidance. Memory use depends on the browser, page, image dimensions, and concurrent work. Start with a conservative configuration, inspect invocation metrics and logs, and tune with representative pages. Large full-page captures and parallel pages increase memory pressure. Keep browser and page lifetimes scoped to the invocation unless you deliberately design and validate reuse.
Timeouts and latency
A request includes cold startup when a new instance starts, browser launch, page navigation, readiness waits, capture, and response transfer. Set bounded navigation and function timeouts with room for the expected page behavior. Avoid unbounded waits. If callers have short deadlines, persist results asynchronously and return a job or object reference instead of holding the HTTP request open.
Reliability
- Close the browser in
finally, including when navigation fails. - Return a clear client error for invalid input and a generic server error for capture failures; log diagnostic details without logging credentials.
- Make retries safe. A screenshot GET may be repeated after client timeouts, so avoid side effects or give persisted jobs an idempotency key.
- Set request and navigation limits, and cap accepted page dimensions or output size where necessary.
- Inspect build logs for dependency/browser installation errors and Cloud Logging for runtime failures.
Cost
Cloud charges depend on the deployed generation, allocated resources, invocation duration, request volume, and any storage or network services you add. The deployment documentation in the research does not establish a cost estimate for this script. Measure the function under your expected workload and use the current Google Cloud pricing information for your project and region.
7. Troubleshooting
| Symptom | Likely cause | What to check or change |
|---|---|---|
Could not find Chrome after deployment |
The browser install was skipped, the cache path differs, or the build did not preserve the downloaded browser. | Inspect build logs and deployed files; confirm puppeteer is a dependency, the install step ran, and .puppeteerrc.js is at the project root. Rebuild with the browser cache available to the runtime. If using puppeteer-core, configure its executable explicitly. |
| Build fails while installing dependencies | Unsupported Node.js version, dependency resolution, or browser download/build failure. | Check the build log’s first error, verify the runtime is currently supported, inspect the lockfile and package versions, and confirm the build environment can complete Puppeteer’s install step. |
| Function fails its startup health check or never becomes ready | Wrong entry point, global-scope crash, initialization timeout, or startup resource exhaustion. | Confirm the deployed entry point matches the exported function name. Inspect Cloud Logging and build logs. Move browser launch and page work into the handler rather than global initialization. Increase resources or timeout only when logs and workload indicate that is needed. |
| Navigation times out | The page is slow, keeps network connections open, or the chosen idle condition is unsuitable. | Use a wait strategy tied to required content, set a bounded navigation timeout, and test the target page’s behavior. Do not simply wait for all network activity to stop on a continuously active site. |
| Screenshot is blank or incomplete | Capture ran before client-rendered content appeared, lazy content was not loaded, or the selector/page state was wrong. | Wait for a meaningful selector or content state, scroll when the page loads content lazily, verify the URL and viewport, and check whether an overlay obscures the content. |
| Invocation runs out of memory | Large page, full-page image, multiple tabs, or excessive concurrency. | Reduce capture dimensions or concurrency, close pages and browsers, and tune memory using observed metrics. Consider storing large outputs rather than returning them inline. |
| Caller receives an HTML error instead of an image | The function returned an error status or platform-generated response. | Check HTTP status and logs before saving the body as an image. Verify the deployed route, trigger, authentication, and query parameter encoding. |
Google’s troubleshooting guidance recommends separating build failures from startup readiness failures: inspect build logs first; when deployment builds but the service does not become ready, inspect Cloud Logging, entry-point configuration, initialization exceptions, crashes, and timeouts. See the Cloud Run functions troubleshooting guide.
8. Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. One GET request returns a PNG, JPEG, WebP, or PDF. Its capture flow accepts cookie and consent banners like a visitor, then removes more than 60 known consent platforms along with newsletter popups and chat widgets; each step can be turned off. Only clean shots are billed: bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and the response identifies the page verdict and billing status in headers. An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for AI agents.
Example using cURL:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python:
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await require('node:fs/promises').writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
See the ScreenshotNeo API documentation for parameters and setup. Cookie banners, popups, and chat widgets are removed before the shot; bot checks, blank pages, and failed loads are never billed; an MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card, and paid plans start at $5 for 3,000. Sign up for free and get 1,000 screenshots a month with no card.
FAQ
Can Google Cloud Functions run headless Chrome?
Puppeteer’s documentation says the Node.js runtime of Google Cloud Functions includes the system packages needed to run Headless Chrome. You still need the browser binary to be installed and available in the deployed package.
Where should Puppeteer store its Chrome cache?
Puppeteer’s Cloud Functions guidance sets the cache under node_modules/.puppeteer_cache in the project. Confirm your build pipeline preserves or recreates the browser there.
Should I use puppeteer or puppeteer-core?
Use puppeteer when you want its install process to download a compatible browser. Use puppeteer-core when you manage the browser binary or connection yourself.
How much memory and timeout does a screenshot function need?
There is no universal setting. Measure browser startup and representative pages, then set resource limits with enough headroom for the largest expected captures and the chosen function generation.
Should the function return the image or a URL?
Return bytes for small synchronous captures. Store the result and return a reference when output size, latency, or asynchronous processing makes an inline response a poor fit.


