How to take a Playwright screenshot in an AWS Lambda function
Capture web pages with Playwright in AWS Lambda: package a compatible Chromium, save screenshots to /tmp, and return them or store them in S3.
To take a Playwright screenshot in AWS Lambda, package a Chromium build and its Linux dependencies for your function’s runtime and architecture, launch it with Playwright, navigate to the page, and call page.screenshot(). Save temporary output under /tmp; return the image only if it fits your invocation interface, or upload it to S3 for durable access. Close the browser in a finally block so a reused execution environment does not accumulate browser processes.
For browser-heavy dependencies, a Lambda container image is a straightforward packaging choice. A ZIP package or layer can also work if the browser and dependencies fit Lambda’s package limits and are built for a compatible Linux environment. The Playwright screenshot API does not guarantee that an arbitrary Chromium installation will run in Lambda: executable path, launch options, system libraries, runtime, and architecture must match your build.
1. Choose how to package Chromium
| Approach | When it fits | What to verify |
|---|---|---|
| Lambda container image | Chromium and system libraries make a ZIP awkward, or you want to control the browser build and OS dependencies. | Use a compatible Lambda base image or include the runtime interface client for another base. Build for the function’s architecture, and ensure the default user can execute the browser. |
| ZIP and layer | The browser and libraries fit within the ZIP limits and can be assembled for a compatible Lambda Linux environment. | Check package size, architecture, runtime compatibility, shared libraries, and whether the package maintaining the browser integration is still active. |
| Hosted browser | You prefer not to bundle Chromium in the function. | Account for the network dependency, vendor operations, data handling, and service terms. Compare cost and latency using your own workload; the available evidence does not establish that a hosted browser is universally faster or cheaper. |
For a container image, use an AWS Lambda language base image or another image configured with the required Lambda runtime components. Build a single-architecture image, such as linux/amd64 or linux/arm64, matching the function. Push the image to ECR in the same Region as the function, then update the Lambda function’s code. Pushing a changed ECR tag alone does not update the deployed function. AWS documents the supported image workflow and base images in its Node.js Lambda container image guide.
Lambda container images must tolerate a read-only filesystem apart from /tmp. Keep browser files readable and executable by the deployed default user. Pin your runtime and browser inputs deliberately, and recheck AWS’s current supported tags and runtime lifecycle when building a new deployment.
2. Implement the Lambda handler
This Node.js handler shows the capture flow and returns the screenshot as base64 in an API Gateway-style response. It assumes that your image contains a compatible Playwright package, Chromium executable, and required Linux libraries. Configure an appropriate Lambda response mode and verify your integration’s payload limit before returning image bytes this way.
const { chromium } = require('playwright');
exports.handler = async (event) => {
let browser;
try {
const url = event?.queryStringParameters?.url ?? event?.url;
if (typeof url !== 'string' || url.length === 0) {
return { statusCode: 400, body: 'Missing url' };
}
// Restrict allowed destinations for your application. Do not expose an
// unrestricted URL fetch endpoint to untrusted callers.
const parsed = new URL(url);
if (!['http:', 'https:'].includes(parsed.protocol)) {
return { statusCode: 400, body: 'Only http and https URLs are supported' };
}
browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
await page.goto(parsed.href, { waitUntil: 'load', timeout: 60000 });
const image = await page.screenshot({
path: '/tmp/screenshot.png',
type: 'png',
fullPage: true
});
return {
statusCode: 200,
headers: { 'content-type': 'image/png' },
isBase64Encoded: true,
body: image.toString('base64')
};
} catch (error) {
console.error('Screenshot capture failed', error);
return { statusCode: 502, body: 'Unable to capture the requested page' };
} finally {
await browser?.close();
}
};
Playwright documents navigation followed by page.screenshot({ path }) and closing the browser in its screenshots guide. The handler above is an implementation outline, not a tested deployment recipe: resolve the Chromium executable and launch options for the exact browser build you package.
Use an appropriate navigation condition
waitUntil: 'load' waits for the page load event. Some applications continue rendering after that event, while pages with long-lived network requests may never become idle. Select readiness based on the target site: wait for a known locator, a short deliberate delay, or network idle only when that condition is meaningful for the page. Prefer a concrete signal to an arbitrary long sleep. Set navigation and function timeouts with enough headroom for navigation, rendering, capture, and output transfer.
Store output in S3 for durable access
/tmp is writable temporary storage for the execution environment, not durable object storage. If callers need to retrieve screenshots later, upload the image to S3 and return an object key or an appropriately controlled URL. Give the Lambda role only the bucket permissions it needs, and decide how objects are named, retained, and accessed.
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
const { chromium } = require('playwright');
const s3 = new S3Client({});
const bucket = process.env.SCREENSHOT_BUCKET;
exports.handler = async (event) => {
let browser;
try {
const url = event?.url;
if (typeof url !== 'string') {
return { statusCode: 400, body: 'Missing url' };
}
const parsed = new URL(url);
if (!['http:', 'https:'].includes(parsed.protocol)) {
return { statusCode: 400, body: 'Unsupported URL protocol' };
}
browser = await chromium.launch({ headless: true });
const page = await browser.newPage();
await page.goto(parsed.href, { waitUntil: 'load', timeout: 60000 });
const image = await page.screenshot({ type: 'png', fullPage: true });
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 }) };
} catch (error) {
console.error('Screenshot capture failed', error);
return { statusCode: 502, body: 'Unable to capture the requested page' };
} finally {
await browser?.close();
}
};
This example uses the AWS SDK for JavaScript v3 S3 client. Add it to your deployment, configure SCREENSHOT_BUCKET, and grant the function role the required write access. Add your own URL policy, object naming strategy, and retention rules before exposing the handler to callers.
3. Configure Lambda for browser work
Browser rendering uses memory and CPU, and screenshot transfer adds time. AWS documents these relevant Lambda limits; check its current quotas when choosing configuration:
| Setting or quota | Documented range or limit | Practical effect |
|---|---|---|
| Function timeout | Up to 900 seconds | Set enough time for browser startup, navigation, page readiness, capture, and delivery. The maximum is a ceiling, not a target. |
| Memory | 128 MB to 10,240 MB | Lambda allocates CPU in proportion to memory. Measure representative pages and tune memory rather than assuming the minimum is enough. |
| Temporary storage | 512 MB to 10,240 MB | Store screenshots and account for browser caches and temporary files under /tmp. |
| ZIP deployment contents | 250 MB uncompressed, including layers | A browser build and libraries may make ZIP packaging difficult. |
| Container image | Up to 10 GB uncompressed | Provides more room for a browser-heavy dependency set. |
| Buffered synchronous payload | 6 MB request and response | Large images may exceed the ordinary buffered response limit. Upload to S3 and return a reference, or use a suitable documented streaming interface. |
These are service quotas, not performance promises. Test cold starts and warm invocations against pages representative of your workload. Avoid retaining page-specific state between invocations unless you intentionally manage its lifecycle; always close the browser, and close contexts or pages if your design reuses a browser process.
4. Tune screenshot behavior
Playwright’s Page screenshot API offers capture options you can select for your use case:
path: write the result to a file, such as/tmp/screenshot.png. You can also omit the path and use the returned image buffer directly.type: choosepngorjpeg. JPEG can reduce output size when lossy compression is acceptable.fullPage: capture the full scrollable page rather than only the viewport. Very long pages can take more time and produce large files.quality: set JPEG quality when using JPEG output; it does not apply to PNG.clip: capture a defined rectangle when you need only a region.omitBackground: omit the default background for supported transparent output, subject to format support.animations: control how animations are handled during capture.scale: choose CSS-pixel or device scale where supported by the Playwright version you package.timeout: bound the screenshot operation separately from navigation.
For an element-only screenshot, locate the element and call its screenshot method, for example await page.locator('main article').screenshot({ path: '/tmp/article.png' }). Check that the selector matches a visible element; dynamic pages may need a locator wait first.
5. Keep the capture endpoint reliable and safe
- Validate destinations. If callers control the URL, allow only the schemes and hosts your service intends to capture. A public screenshot endpoint can otherwise become an unrestricted fetch proxy. Consider redirects and destinations that resolve to private network addresses as part of your URL policy.
- Bound work. Set navigation and capture timeouts, function timeout, and limits on full-page output. Return a controlled error when startup, navigation, or capture fails.
- Clean up resources. Close the browser in a
finallyblock even after failures. If creating multiple contexts, close each context as well. - Make delivery explicit. Return image bytes only when the invocation path supports their size. Use S3 for durable output and least-privilege IAM permissions.
- Log useful failure details. Record a request identifier and failure stage without logging secrets, authorization headers, or sensitive page content.
- Check architecture and libraries. Build and deploy for the same architecture, and verify the executable and its shared libraries inside the actual Lambda image.
6. Troubleshoot common failures
| Symptom | Likely cause | Fix |
|---|---|---|
| Browser executable not found | Playwright is present but the corresponding Chromium build is absent, installed in another path, or incompatible with the package. | Package a matching browser build, verify its executable path and permissions, and configure the launch path/options required by that build. |
| Shared library or launch error | Chromium’s Linux dependencies are missing or incompatible with the image’s operating system. | Build against a compatible Lambda Linux base and include the required libraries. Test the image with its deployed user and architecture. |
| Works locally but fails in Lambda | Local OS, CPU architecture, filesystem permissions, or installed libraries differ from the function environment. | Build for the target architecture and run the packaged image in an environment that matches Lambda. Check read-only filesystem assumptions and write temporary data only under /tmp. |
| Function times out during navigation | The page is slow, readiness waits for a condition that never occurs, or the function has too little time for rendering and delivery. | Choose a page-specific readiness condition, use bounded timeouts, measure representative pages, and configure adequate function time and memory. |
| Screenshot is blank or incomplete | The page renders asynchronously, requires interaction, or the chosen load event occurs before the relevant content appears. | Wait for a known locator or application readiness signal. If necessary, interact with the page before capture. Avoid assuming that load means all application content is ready. |
| Response is rejected or truncated | The image exceeds the invocation integration’s payload limit. | Upload to S3 and return a key or controlled URL, reduce dimensions or output size, or select an appropriate streaming response mechanism. |
| Cannot write screenshot file | The code writes outside the writable temporary directory or temporary storage is exhausted. | Write under /tmp, increase configured ephemeral storage if needed, and remove temporary files when appropriate. |
| New image code does not appear in Lambda | The ECR tag changed, but the function still references the prior deployed image digest. | Update the Lambda function code after pushing the image. |
| S3 upload is denied | The execution role lacks permission for the target bucket or object path. | Grant only the required S3 write permission to the function role and verify bucket and key configuration. |
7. Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server for developers. One GET request returns a PNG, JPEG, WebP, or PDF. Its API accepts many parameter names used by other screenshot APIs, which can make switching easier. 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
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)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture; each cleanup step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers report the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots. Every feature is on every plan.
Sign up free for 1,000 screenshots a month with no card.
Frequently asked questions
Can I reuse a browser between Lambda invocations?
A warm execution environment may be reused, but browser reuse adds lifecycle and isolation concerns. Start with one browser per invocation and close it reliably. If you later reuse a process, manage page and context cleanup, stale browser failures, and separation of caller data explicitly.
Should I use a layer or a container image?
Choose based on package size, browser and library control, runtime compatibility, and maintenance effort. A container is often simpler for a large browser dependency set; a ZIP or layer is viable when the complete bundle fits and matches Lambda’s environment.
Is the Lambda maximum timeout a good target?
No. It is an upper limit. Set a timeout based on measured navigation, rendering, capture, and delivery time, with enough headroom for normal variation.
Where should I store screenshots users need later?
Use durable object storage such as S3. Treat /tmp as temporary function storage, and return a reference to the stored object instead of relying on a local file after the invocation.


