Puppeteer Screenshot on AWS Lambda: Setup and Common Errors
Deploy Puppeteer screenshots on AWS Lambda with compatible Chromium, a runnable handler, packaging options, and fixes for the errors developers hit most.
Direct answer: To take a Puppeteer screenshot on AWS Lambda, deploy a Linux-compatible Chromium build and Puppeteer as a matched unit, launch Chromium using the package’s Lambda-specific executable path and arguments, write temporary files under /tmp, and save the result to durable storage such as S3. A local Chrome installation is not a Lambda deployment. For ZIP or layer deployments, Puppeteer’s troubleshooting guide points Lambda users to Sparticuz Chromium; container images are another option. Pin and verify compatible versions for your Lambda runtime and architecture.
This guide uses a ZIP deployment with @sparticuz/chromium and puppeteer-core. It covers the handler, packaging choices, Lambda settings, common errors, and operational tradeoffs.
1. Choose a deployment package
Decide how Chromium and its operating-system dependencies will reach the function before writing launch code. They must be present in the deployed environment, match its Linux architecture, and work with the Puppeteer version you install.
| Approach | What it means | Check carefully |
|---|---|---|
| ZIP with Chromium package | Bundle the Node packages and Chromium resources with the function. | Artifact size, runtime compatibility, architecture, and resource paths. |
| ZIP with a layer or remote pack | Keep some Chromium assets outside the function package. Sparticuz’s -min package omits compressed Chromium files, which you must provide separately, for example in /opt/chromium. |
That the external assets exist at the path expected by the package at runtime. |
| Container image | Package the application, browser, and required operating-system libraries in an image. | Use a currently supported Lambda base image and keep the image’s browser and Puppeteer versions compatible. |
AWS has a container-image example that demonstrates the workflow of launching Puppeteer in Lambda and writing screenshots to S3. Its Dockerfile uses the historical Node.js 12 base image, so treat it as an architecture example rather than a current runtime recipe: AWS browser automation example. For the current package options and compatibility notes, consult the Sparticuz Chromium documentation and Puppeteer’s troubleshooting guide.
Match architecture and browser assets
Do not deploy a macOS or Windows browser binary to Lambda. Sparticuz documents x64 binaries in its npm package. For arm64, its README describes using the -min package with an arm64 Lambda layer or remote pack; it notes arm64 binaries are available beginning with Chromium v135. Select the Lambda architecture and the corresponding artifact together, then check the package’s current release notes before pinning it.
2. Install compatible dependencies
Use puppeteer-core when you supply Chromium separately. Pin dependency versions in your lockfile so a deployment does not silently change its browser package. The following is an example starting point; confirm compatibility with the versions you choose before deployment.
npm install puppeteer-core @sparticuz/chromium
npm install --save-dev esbuild
For a simple ZIP deployment, include the production dependencies in the artifact. If your bundler packages the handler, mark @sparticuz/chromium external so it can resolve its resource files at runtime. Do not bundle away or omit the files supplied by your selected package, layer, or remote pack.
3. Create a screenshot handler
This Node.js handler accepts a URL, captures a full-page PNG, uploads it to S3, and returns the object key. Configure the function role with the permissions needed to write to the chosen bucket; keep permissions limited to the required bucket and operation. Pass input through your application’s normal validation and authorization before using it.
import chromium from '@sparticuz/chromium';
import puppeteer from 'puppeteer-core';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
const s3 = new S3Client({});
export const handler = async (event) => {
const bucket = process.env.SCREENSHOT_BUCKET;
if (!bucket) throw new Error('SCREENSHOT_BUCKET is not configured');
const input = typeof event.body === 'string'
? JSON.parse(event.body)
: (event.body ?? event);
const url = input.url;
if (typeof url !== 'string' || !/^https?:\/\//i.test(url)) {
return { statusCode: 400, body: JSON.stringify({ error: 'Provide an http or https URL' }) };
}
// Keep browser configuration and temporary profile data in writable Lambda storage.
process.env.XDG_CONFIG_HOME = '/tmp/.chromium';
process.env.XDG_CACHE_HOME = '/tmp/.cache';
let browser;
try {
const executablePath = await chromium.executablePath();
browser = await puppeteer.launch({
args: chromium.args,
defaultViewport: chromium.defaultViewport,
executablePath,
headless: true,
userDataDir: '/tmp/puppeteer-profile'
});
const page = await browser.newPage();
await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 });
const image = await page.screenshot({ fullPage: true, type: 'png' });
const key = `screenshots/${Date.now()}.png`;
await s3.send(new PutObjectCommand({
Bucket: bucket,
Key: key,
Body: image,
ContentType: 'image/png'
}));
return { statusCode: 200, body: JSON.stringify({ bucket, key }) };
} finally {
if (browser) await browser.close();
}
};
The code assumes an ESM Node.js handler and the AWS SDK v3 S3 client dependency. Configure SCREENSHOT_BUCKET in the Lambda environment. The returned object key is not itself a public URL; grant access or create a signed URL through your application’s storage flow if a caller needs to retrieve the image.
networkidle2 waits until network activity is sufficiently quiet, but pages with polling or long-lived requests may not reach that state. If that causes timeouts, wait for a more appropriate condition such as domcontentloaded and then wait for a page-specific selector or a bounded delay. Always set timeouts deliberately and test pages representative of your workload.
4. Bundle and deploy the function
Bundler configuration
With esbuild, externalize the Chromium package rather than folding it into the bundle. Include it as a production dependency in the deployment artifact so its resource lookup can work.
npx esbuild src/handler.js \
--bundle \
--platform=node \
--target=node20 \
--format=esm \
--external:@sparticuz/chromium \
--outfile=dist/handler.mjs
Adjust the target to the Node.js runtime actually configured for the function. After packaging, inspect the artifact and confirm the handler, Chromium package, and any separately supplied layer or remote resources are present at the expected locations. If using a layer, verify the deployed layer version and mount path as well.
Container alternative
A container can keep the browser and system libraries together with the handler. Base it on a Lambda container image for a currently supported runtime, install compatible browser dependencies during the image build, and push the image using your normal Lambda deployment workflow. AWS’s sample illustrates the pattern and an optional fan-out worker design for many URLs, but its old Node.js 12 base image must not be copied as current guidance.
5. Configure Lambda for the workload
- Memory and CPU: Lambda allocates CPU in relation to configured memory. Browser startup, page complexity, image size, and concurrency all affect resource use. Test and tune with representative pages instead of assuming a universal memory value.
- Timeout: Set enough time for browser startup, navigation, rendering, screenshot encoding, and storage writes. Lambda stops an invocation at its configured timeout. Test realistic slow pages and expected upper-bound workloads.
- Temporary storage: Use
/tmpfor writable browser configuration, cache, profile, and temporary files. Account for the files your chosen Chromium package extracts or creates. - Architecture: Match the function’s x64 or arm64 setting with the corresponding Chromium artifact and compatible Node dependencies.
- Environment: Set the output bucket and any application configuration explicitly. Never put credentials for target websites in source code or log sensitive request headers.
- Concurrency: For batches, bound parallel browser work and downstream storage requests. AWS’s sample separates a fan-out function from per-URL screenshot workers; choose an orchestration pattern appropriate to your throughput and retry needs.
6. Diagnose common errors
| Symptom | Likely cause | What to check or change |
|---|---|---|
Chromium exits before Puppeteer connects; crashpad says --database is required |
Browser config, cache, or profile paths are not writable in the execution environment. | Set XDG_CONFIG_HOME and XDG_CACHE_HOME to directories under /tmp. Set userDataDir under /tmp when needed, and ensure the directories can be created. |
The input directory "/var/task/bin" does not exist |
Bundling prevented Sparticuz Chromium from resolving its package resources. | Externalize @sparticuz/chromium in the bundler, include the dependency in the artifact, and inspect deployed paths. |
| Text is missing or glyphs look different | The Lambda image does not have the font faces available on your development machine. | Sparticuz includes Open Sans coverage for Latin, Greek, and Cyrillic. Add the required fonts in a Lambda layer or another documented font location such as /opt/fonts, /var/task/fonts, or /tmp/fonts, then render again. |
| Invocation times out | Navigation, browser work, transfer, or output storage exceeds the timeout, or the workload needs more resources. | Check Lambda timeout and memory, page/network latency, wait condition, image dimensions, and storage latency. Use realistic workload tests to tune settings. |
| Warm invocations slow down or consume more resources | State or resources retained across invocations may accumulate. | Close pages and await browser.close() in a finally block. Inspect module-level globals and libraries that retain memory across warm invocations. |
| Screenshot object is missing | The handler may have failed before capture or upload completed. | Inspect the Lambda invocation result and CloudWatch Logs. Check the exact failing stage: launch, navigation, screenshot, or S3 upload, then verify the bucket configuration and role permissions. |
| Browser starts locally but fails in Lambda | The local browser, OS libraries, architecture, writable paths, or package versions differ from the deployment. | Reproduce with the deployed Linux architecture and package set. Verify the executable path, Chromium artifact, bundling, and writable directories instead of relying on local Chrome. |
Use logs that identify the failing stage without recording secrets or full sensitive page content. A longer timeout will not fix a missing executable, a wrong architecture, unwritable paths, or an incompatible browser build; find the first failing step and address that cause.
7. Performance, reliability, and cost
Measure the actual capture path
Track browser launch, navigation, screenshot, and storage time separately. Use a representative mix of page sizes, scripts, fonts, and network behavior, including slow or dynamic pages. Tune memory and timeout based on observed upper-bound work; the available sources establish no universal Puppeteer latency or throughput figure.
Make failures recoverable
Close the browser in all paths, keep navigation waits bounded, and make output naming and retries safe for your application. For batch capture, limit concurrency and consider separating orchestration from workers. Do not assume that a successful local run proves the deployed artifact is complete.
Understand the cost inputs
Lambda invocation duration and configured resources, image or storage operations, and any orchestration all contribute to the deployment’s cost. Browser startup and rendering work can vary by page and environment. Measure representative traffic and check current AWS pricing for the region and architecture you use; this guide does not claim a universal per-screenshot cost.
8. Or skip the browser setup
If you need screenshots without managing Chromium packaging and Lambda browser startup, ScreenshotNeo is a website screenshot API and MCP server from Yorker Media. One GET request returns a PNG, JPEG, WebP, or PDF. Its capture flow accepts cookie banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before the shot; each step can be turned off. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and responses identify the page verdict and billing status in headers. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. There are 1,000 screenshots per month free with no card; paid plans start at $5 for 3,000.
For the complete request options, see the ScreenshotNeo API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
Sign up for 1,000 free screenshots a month with no card.
FAQ
Can I use the Chrome installed on my laptop?
No. Lambda needs a browser binary and runtime environment compatible with its Linux environment and architecture. Package or otherwise provide a matching Chromium build.
Should I use a container, a layer, or a ZIP?
Choose based on artifact size, operating-system dependencies, and how much external asset management your deployment can handle. Verify the complete artifact and benchmark it in the target environment.
Can the handler return an image directly?
It can, if your invocation and response path supports the image payload and size. The example stores the PNG in S3 and returns a key, keeping durable output retrieval separate from the browser invocation.
Why do I get different text than in local screenshots?
Fonts differ between environments. Provision the needed font faces in Lambda and confirm that the page has finished rendering before capture.


