How to Capture a Website Screenshot with Puppeteer on AWS Lambda
Build a Lambda function that captures website screenshots with Puppeteer Core and Chromium, with practical guidance on packaging, configuration, delivery, and troubleshooting.
To capture a website screenshot on AWS Lambda, deploy puppeteer-core with a Lambda-compatible Chromium binary, launch Puppeteer using that binary’s arguments and executable path, navigate to a validated URL, and call page.screenshot(). The example below returns a PNG as a base64 Lambda response. For larger or durable images, write the screenshot to S3 and return an object key or an application-controlled URL.
Use the same compatible versions of Puppeteer, Chromium, Node.js runtime, and CPU architecture in your build and deployment. Package-specific guidance can change, so verify the selected release’s documentation rather than assuming one configuration works for every Lambda function.
1. Choose the deployment and browser combination
Puppeteer Core controls a browser but does not download one for you. A package such as @sparticuz/chromium supplies a Chromium binary and launch settings designed for serverless use. Its README describes the launch pattern used below. Check that the exact package release supports your Lambda runtime and architecture, and that its Chromium version is compatible with your Puppeteer version.
| Decision | Use when | Trade-off to account for |
|---|---|---|
| ZIP deployment | Your dependencies and browser assets fit the deployment-package limits and your build can include them reliably. | Browser binaries make package size and bundler configuration important. |
| Container image | You need more room for browser dependencies or a controlled operating-system environment. | You maintain and publish an image and its runtime contents. |
| Bundled browser package | You want the browser package to provide its binary assets and launch defaults. | Follow its bundler and architecture instructions; a missing binary resource can prevent launch. |
| External pack or layer | Your selected architecture or package release calls for browser assets to be supplied separately. | Keep the external pack or layer aligned with the Chromium and Puppeteer versions. |
AWS documents a 50 MB limit for ZIP uploads through the console, API, or SDK and a 250 MB unzipped deployment-package contents limit, including layers and custom runtimes. Lambda container images can be up to 10 GB uncompressed. Check the current [Lambda quotas](https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html) as part of deployment planning.
The current @sparticuz/chromium README documents x64 binaries. For arm64, it points to @sparticuz/chromium-min with an arm64 layer or remote pack. Confirm the instructions for the precise release you choose. The README also warns that its versioning follows Chromium releases rather than semantic versioning, so check for breaking changes even at patch-level upgrades. See the [Sparticuz Chromium README](https://github.com/Sparticuz/chromium).
2. Create a small Lambda project
This example uses an ES module. Choose a currently supported Node.js Lambda runtime and package versions compatible with it. For an npm project, set "type": "module" in package.json so the import statements work.
{
"type": "module",
"dependencies": {
"@sparticuz/chromium": "YOUR_COMPATIBLE_VERSION",
"puppeteer-core": "YOUR_COMPATIBLE_VERSION"
}
}
Replace both placeholders with versions you have checked for runtime, browser, and architecture compatibility, then install dependencies and include them in the deployment artifact. The version placeholders are intentional; no single version pair is guaranteed to be suitable for every deployment.
3. Implement the screenshot handler
The handler validates the input URL, sets a navigation timeout, requests a PNG, and closes the browser in a finally block. It uses networkidle0 as an example navigation condition; some pages keep connections open or never become idle, so choose and document a wait condition that suits your targets.
import puppeteer from "puppeteer-core";
import chromium from "@sparticuz/chromium";
const NAVIGATION_TIMEOUT_MS = 45_000;
function parseTargetUrl(value) {
if (typeof value !== "string" || value.length === 0 || value.length > 2_048) {
throw new Error("url must be a non-empty string no longer than 2048 characters");
}
let parsed;
try {
parsed = new URL(value);
} catch {
throw new Error("url must be an absolute URL");
}
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
throw new Error("url must use http or https");
}
return parsed.href;
}
export const handler = async (event = {}) => {
let targetUrl;
try {
targetUrl = parseTargetUrl(event.url);
} catch (error) {
return {
statusCode: 400,
headers: { "content-type": "application/json" },
body: JSON.stringify({ error: error.message }),
};
}
let browser;
try {
browser = await puppeteer.launch({
args: chromium.args,
defaultViewport: chromium.defaultViewport,
executablePath: await chromium.executablePath(),
headless: chromium.headless,
});
const page = await browser.newPage();
page.setDefaultNavigationTimeout(NAVIGATION_TIMEOUT_MS);
await page.goto(targetUrl, { waitUntil: "networkidle0" });
const screenshot = await page.screenshot({
type: "png",
fullPage: true,
});
return {
statusCode: 200,
headers: { "content-type": "image/png" },
body: screenshot.toString("base64"),
isBase64Encoded: true,
};
} catch (error) {
console.error("Screenshot capture failed", error);
return {
statusCode: 502,
headers: { "content-type": "application/json" },
body: JSON.stringify({ error: "Could not capture the requested page" }),
};
} finally {
if (browser) {
await browser.close().catch((error) => {
console.error("Could not close Chromium", error);
});
}
}
};
The URL check here restricts the scheme and basic input shape. If callers can supply arbitrary URLs, add application-specific host restrictions and network controls: syntax validation alone does not prevent requests to internal or otherwise sensitive destinations. Avoid returning raw browser error details to untrusted callers.
What the handler configures
args,defaultViewport,executablePath, andheadlesscome from the Chromium package. The executable path is asynchronous and must be awaited.waitUntil: "networkidle0"waits for network activity to become idle according to Puppeteer’s navigation behavior. Analytics, streaming, and long-lived requests can make this unsuitable for some pages.fullPage: truecaptures the full page. Set it tofalsefor the current viewport. Very long pages can produce large images and consume more memory.type: "png"controls the image format. Puppeteer also supports JPEG and WebP in versions that provide those screenshot options; check the selected Puppeteer release and set quality where supported for lossy output.setDefaultNavigationTimeoutbounds navigation waiting. Set the Lambda timeout longer than the expected navigation and screenshot work, with time left for cleanup and response handling.
4. Set Lambda memory, timeout, and temporary storage
AWS documents Lambda memory from 128 MB to 10,240 MB, and CPU power scales with the configured memory. The standard function timeout can be up to 900 seconds. Browser work varies with page scripts, images, fonts, viewport size, and concurrency, so there is no universally sufficient setting. The Sparticuz Chromium README advises at least 512 MB RAM and recommends 1,600 MB or more; treat that as package guidance and measure your own workload.
Lambda’s configurable /tmp storage ranges from 512 MB to 10,240 MB. Chromium extracts browser files there on first use and can reuse the extracted binary in a warm execution environment. Ensure the available space covers browser files, profile data, and any output files you write. Lambda documents that data in /tmp is temporary and unique to an execution environment; delete generated files when they are no longer needed. See AWS documentation for [Lambda quotas](https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html) and [ephemeral storage](https://docs.aws.amazon.com/lambda/latest/dg/configuration-ephemeral-storage.html).
5. Choose how to deliver the image
Return a modest image directly
The handler returns the PNG bytes as base64 and sets isBase64Encoded: true. This works when the invoking integration accepts that response shape and the encoded body fits the synchronous request and response quotas. Base64 increases the body size, so check the current [Lambda quotas](https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html) and any limits in the service in front of Lambda. Large full-page images can exceed those limits.
Write durable output to S3
For persistent output or images too large for a synchronous response, save the screenshot to S3 and return an object key or a URL created according to your application’s access policy. Grant the Lambda execution role only the S3 permissions it needs. AWS has described this screenshot-to-S3 architecture in an [AWS Architecture Blog example](https://aws.amazon.com/blogs/architecture/). That example demonstrates the pattern; use a current Lambda runtime and current deployment instructions.
import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
const s3 = new S3Client({});
const bucket = process.env.SCREENSHOT_BUCKET;
// After creating `page` and navigating to the target:
const image = await page.screenshot({ type: "png", fullPage: true });
const key = `screenshots/${crypto.randomUUID()}.png`;
await s3.send(new PutObjectCommand({
Bucket: bucket,
Key: key,
Body: image,
ContentType: "image/png",
}));
return {
statusCode: 200,
headers: { "content-type": "application/json" },
body: JSON.stringify({ key }),
};
Install @aws-sdk/client-s3 and configure SCREENSHOT_BUCKET when using this snippet. It assumes the bucket, role permissions, and output access policy are set up in your AWS account.
6. Package browser assets correctly
If you bundle with esbuild or webpack, the Sparticuz README says to externalize @sparticuz/chromium because it locates binary resources by relative paths. A /var/task/bin missing error can indicate the package was bundled without its expected resources. Include the binary assets using the package’s documented method, a Lambda layer, or an external pack as appropriate. Confirm the resulting artifact actually contains what the deployed launch path expects.
With ZIP deployments, inspect the archive contents and uncompressed size, including layers. With container images, make sure the browser and libraries are present in the image for the chosen runtime and architecture. If using an external pack or remotely supplied assets, account for availability and download time within the invocation design.
7. Develop locally without confusing browser paths
The Sparticuz Chromium package’s bundled binary is Linux-only and does not run directly on macOS or Windows. For local development, use a locally installed browser and make the production Lambda path explicit. For example, select a local executable only when a local environment variable is set; do not accidentally deploy a developer machine’s browser path.
const isLocal = process.env.LOCAL_BROWSER === "1";
const launchOptions = isLocal
? {
headless: true,
executablePath: process.env.LOCAL_CHROMIUM_PATH,
}
: {
args: chromium.args,
defaultViewport: chromium.defaultViewport,
executablePath: await chromium.executablePath(),
headless: chromium.headless,
};
const browser = await puppeteer.launch(launchOptions);
Set LOCAL_CHROMIUM_PATH to a browser executable installed on your development machine. Launch flags can differ by browser build and operating system, so keep local settings scoped to local use.
8. Common problems and fixes
| Symptom | Likely cause | What to check |
|---|---|---|
| Chromium cannot launch or executable is missing | The binary package or its assets are absent, the path was not awaited, or the deployed architecture does not match. | Check deployment contents, await chromium.executablePath(), architecture, and package instructions. |
/var/task/bin is missing |
A bundler did not preserve the Chromium package’s relative binary resources. | Externalize @sparticuz/chromium as its README directs, or use its documented asset packaging method. |
| Navigation times out | The page is slow, keeps network requests open, or the chosen idle condition is never reached. | Inspect the target and navigation logs, set an appropriate timeout, and consider a different wait condition or a deliberate selector/delay strategy. |
| Invocation runs out of time or memory | Rendering cost exceeds the current function allocation or the page is unusually heavy. | Measure representative pages, increase memory and timeout within AWS limits, and consider reducing viewport or capture scope. |
| Temporary storage fills | Browser extraction, profiles, or generated artifacts exceed available /tmp space. |
Review ephemeral storage settings and file lifecycle; remove artifacts no longer needed. |
| Works locally but fails in Lambda | Local browser, OS libraries, package contents, or architecture differ from deployment. | Check runtime, architecture, binary package, and production launch branch together. |
| Upgrade breaks launch | The selected Chromium package may change incompatibly with Chromium releases. | Review release notes and verify Puppeteer/browser compatibility before deploying the upgrade. |
| Image response is rejected or truncated | Base64 output exceeds an integration or synchronous payload limit. | Check payload quotas and switch to S3 output for larger or persistent screenshots. |
9. Performance, reliability, and cost considerations
- Memory and CPU: More Lambda memory also means more CPU allocation. Compare latency and invocation cost using your own representative pages and concurrency rather than assuming one size is optimal.
- Cold and warm environments: The package extracts Chromium into
/tmpon first use and can reuse it in a warm environment. Do not treat warm reuse as guaranteed across invocations. - Page variability: Third-party scripts, large images, fonts, and dynamic content affect completion time and image size. Pick a viewport, full-page policy, and wait condition that meet the capture need.
- Failure handling: Set navigation timeouts, return controlled errors, log useful diagnostics without exposing sensitive details, and close the browser even when navigation or capture fails.
- Concurrency and external sites: Each invocation may create browser work and outbound requests. Consider the target sites’ capacity and your account’s concurrency needs. If the function runs in a VPC and must reach public sites, verify that its network design provides the required outbound access.
- Cost: Lambda cost depends on allocated memory and execution duration, alongside deployment and storage choices. S3 storage and requests add their own costs. Estimate using actual invocation patterns and current AWS pricing.
Or skip the browser setup
ScreenshotNeo takes a website screenshot with one API request, and its [API documentation](https://screenshotneo.com/docs/) covers the request options.
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
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}`);
const bytes = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(({ writeFile }) => writeFile('shot.webp', bytes));
ScreenshotNeo removes cookie banners, popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots. See [ScreenshotNeo](https://screenshotneo.com) and [create a free account](https://screenshotneo.com/account/sign-up/).
Frequently asked questions
Can I use Puppeteer instead of Puppeteer Core?
The example uses Puppeteer Core because the deployment supplies its own compatible browser executable. Choose a package strategy deliberately so you do not accidentally bundle an incompatible or unnecessary browser download.
Does a screenshot always show the whole page?
No. The example sets fullPage: true. Set it to false to capture the viewport, and account for the larger output and rendering work a very long page can require.
Should the handler return an image or an S3 key?
Return image bytes when the output is modest and the invocation path accepts the payload. Use S3 when the image should persist or could exceed response limits.
Can this capture a URL supplied by an API caller?
It can, but validate and constrain destinations according to your application’s security requirements. Accepting arbitrary URLs can expose resources reachable from the function’s network environment.
Will local development use the same Chromium binary?
Not necessarily. The Sparticuz bundled binary is Linux-only. Use an installed local browser for macOS or Windows development and keep that path separate from the Lambda configuration.


