How to Take a Full-Page Website Screenshot in Node.js Using Puppeteer on AWS Lambda
Capture full-page website screenshots in a Node.js Lambda with Puppeteer and compatible Chromium. Learn setup, readiness checks, deployment, storage, and troubleshooting.
Use Puppeteer’s page.screenshot({ fullPage: true }) in a Node.js Lambda handler, launched with a Chromium binary that works in Lambda. Navigate to the page, wait for the content your capture needs, take the screenshot, then return the image bytes or store them and return a reference. The example below uses puppeteer-core and @sparticuz/chromium.
This is an implementation pattern, not a tested deployment template. Pin compatible versions and verify your runtime, architecture, packaging, and response path before shipping. See the Puppeteer screenshot API, Sparticuz Chromium documentation, and AWS Lambda quotas.
1. Install compatible browser dependencies
Lambda does not provide the desktop Chrome installation expected by a typical Puppeteer setup. Use a Lambda-compatible Chromium distribution and launch it through puppeteer-core. Puppeteer’s troubleshooting documentation points Lambda users to Sparticuz Chromium. Check that package’s current release instructions for your Node.js runtime and architecture.
npm install puppeteer-core @sparticuz/chromium
Pin dependency versions in your lockfile. Browser packages and Puppeteer releases must be compatible; their runtime and browser requirements can change. For example, Puppeteer 25.12.0 documents Node.js 22.12 or later and Chrome for Testing 154.0.8037.57. Treat those as version-specific facts and check the current system requirements and browser compatibility table when choosing your versions.
2. Create a Lambda handler that captures the full page
This ES module handler takes a URL from the event, waits for network activity to settle, captures the full page as PNG, and returns the bytes as base64. Configure the function’s handler entry point for the filename and export you use.
import puppeteer from 'puppeteer-core';
import chromium from '@sparticuz/chromium';
export const handler = async (event) => {
const url = event?.url;
if (typeof url !== 'string' || !url.startsWith('https://')) {
return {
statusCode: 400,
body: 'Provide an HTTPS URL in event.url',
};
}
const browser = await puppeteer.launch({
args: chromium.args,
executablePath: await chromium.executablePath(),
headless: true,
});
try {
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 900 });
await page.goto(url, {
waitUntil: 'networkidle2',
timeout: 60000,
});
const image = await page.screenshot({
type: 'png',
fullPage: true,
});
return {
statusCode: 200,
headers: { 'content-type': 'image/png' },
isBase64Encoded: true,
body: Buffer.from(image).toString('base64'),
};
} finally {
await browser.close();
}
};
For CommonJS projects, use the package exports supported by the dependency versions you pinned and export the handler using your runtime’s expected convention. The exact import form is package-version dependent; check each package’s documentation instead of assuming the example’s ES module syntax transfers unchanged.
What the important lines do
chromium.argssupplies Chromium launch arguments for the serverless package.await chromium.executablePath()prepares and returns the browser executable path. Sparticuz documents extracting its files under/tmpon first run.waitUntil: 'networkidle2'waits for a navigation condition; it does not guarantee that every application-rendered or lazy-loaded element is ready.fullPage: truerequests a full-page capture. The screenshot API returns bytes when no output path is supplied.finallycloses Chromium even if navigation or capture fails.
3. Choose a page readiness strategy
The correct wait depends on the target page. Puppeteer’s screenshot guide demonstrates networkidle2, but a site can keep network connections open or render important content after navigation. A navigation event alone does not prove that your desired content is present.
| Strategy | Use when | Watch for |
|---|---|---|
waitUntil: 'load' |
The page’s load event is a reasonable capture boundary. | Client-side rendering or later requests may still be in progress. |
waitUntil: 'domcontentloaded' |
You need the initial document parsed quickly and will wait for application readiness separately. | Images, styles, and asynchronous content may not be ready. |
waitUntil: 'networkidle2' |
The page becomes quiet after its main requests finish. | Persistent requests may delay or prevent the condition; lazy content may not have been requested. |
| Wait for a selector | A specific element means the content you need has rendered. | The selector must be stable and the element must appear before the timeout. |
| Application readiness check | Your own site exposes a known state or global when data is ready. | Keep the check bounded by a timeout. |
For example, replace the navigation wait with a site-specific selector check after navigation:
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 });
await page.waitForSelector('main article', { timeout: 20000 });
const image = await page.screenshot({ type: 'png', fullPage: true });
For pages that load content as the visitor scrolls, scroll in increments and allow each segment to render before capturing. This is site-specific behavior: Puppeteer’s navigation wait does not automatically guarantee that offscreen lazy-loaded images have been requested.
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 });
await page.waitForSelector('main', { timeout: 20000 });
await page.evaluate(async () => {
const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const step = Math.max(300, Math.floor(window.innerHeight * 0.75));
for (let y = 0; y < document.body.scrollHeight; y += step) {
window.scrollTo(0, y);
await pause(150);
}
window.scrollTo(0, 0);
});
await page.waitForTimeout(500);
const image = await page.screenshot({ type: 'png', fullPage: true });
Scrolling every page can add substantial time. Use it only when the target relies on scroll-triggered loading, and consider waiting for specific image or content conditions where possible.
4. Package and configure Lambda
Choose a deployment format based on artifact size and how much control you need over the runtime image.
| Deployment route | Documented limit | Practical consideration |
|---|---|---|
| ZIP package uploaded directly through Lambda API or SDK | 50 MB zipped | Larger uploads can use S3; account for browser dependencies in the artifact. |
| ZIP package including layers | 250 MB unzipped | The total includes layers. Check the final extracted package size. |
| Container image | 10 GB uncompressed | Provides more room and build control, with a different image build and deployment workflow. |
These are AWS-documented limits for the listed deployment forms. Review the current Node.js ZIP packaging guide and Lambda quotas for details before deployment.
Memory, CPU, timeout, and temporary storage
- Standard Lambda functions support configurable memory from 128 MB to 10,240 MB. AWS allocates CPU in proportion to memory; 1,769 MB corresponds to one vCPU.
- The standard function maximum execution duration is 900 seconds.
/tmpstorage is configurable from 512 MB to 10,240 MB. Browser extraction and generated artifacts use temporary storage.
There is no universal memory, timeout, or temporary-storage setting for every page. Start with a bounded timeout and enough temporary space for the extracted browser and output, then measure representative pages, including long pages and pages with heavy assets. Higher memory also changes available CPU, so benchmark your own workload before settling on a configuration.
5. Return the image or store it
The example returns base64-encoded image bytes. That is convenient for an invocation path that accepts the response, but the encoded payload is larger than the PNG itself. AWS lists a 6 MB synchronous request/response payload quota and streamed synchronous responses up to 200 MB, subject to the service details. Check the limits for the specific trigger or gateway in your architecture.
For larger captures or workflows where a durable result is needed, upload the buffer to object storage and return a storage reference. The storage SDK, bucket configuration, access policy, and URL behavior depend on your application and are not shown here. Keep only the necessary artifacts in /tmp, and handle upload failures as part of the invocation’s error path.
Always close the browser in a finally block. Reusing an execution environment can make warm invocations faster, but browser reuse requires careful cleanup of pages and state between requests. A fresh browser per invocation is simpler to reason about; measure cold starts and throughput for your workload before introducing reuse.
6. Troubleshoot common failures
| Symptom | Likely cause | Fix |
|---|---|---|
| Executable not found or launch fails | Chromium was not packaged or extracted as expected, or the selected build does not match the runtime or architecture. | Use the package’s documented launch arguments and executable path. Verify runtime, architecture, dependency versions, and /tmp availability. |
| Function package exceeds size limit | The browser binary plus dependencies exceed the ZIP or layer limit. | Inspect packaged and uncompressed sizes. Choose an S3 upload for a larger ZIP where supported, reduce unnecessary dependencies, or use a container image within its documented limit. |
| Timeout during navigation | The site is slow, keeps requests open, or the wait condition is too strict for it. | Set an explicit navigation timeout, select a suitable wait strategy, and wait for a meaningful selector or application state. Increase the Lambda timeout only after measuring the workload. |
| Screenshot is missing below-the-fold content | Lazy-loaded elements may not have loaded before capture. | Scroll the page to trigger loading, wait for required content, and then capture. Confirm the page height and target elements before screenshotting. |
| Blank or incomplete screenshot | The capture happened before client-side rendering finished, or the page returned an error state. | Wait for a stable selector or application readiness condition. Log the final URL and inspect response status and page content in a controlled diagnostic run. |
| Image response rejected or truncated | Response payload exceeds the quota of the invocation path. | Store the image and return a reference, or use a supported response streaming path after checking its limits. |
| Temporary storage exhausted | Chromium extraction and artifacts need more space than configured. | Raise ephemeral storage within Lambda’s available range, clean temporary output, and avoid accumulating files across warm invocations. |
| Memory error or browser crash on long pages | The page, browser, or full-page rasterization requires more memory than configured. | Measure with representative pages, increase memory where justified, reduce unnecessary page resources, or capture/store a smaller output if the use case permits. |
| Browser remains active after a failed capture | Cleanup was skipped on an exceptional path. | Keep browser closure in finally; if managing multiple pages or contexts, close those deliberately too. |
7. Performance, reliability, and cost considerations
Performance
Capture time includes browser startup, navigation, readiness waits, rendering, image encoding, and any upload. Large pages and full-page rasterization can use more memory and time than a viewport capture. Avoid waiting for a stronger condition than the page needs, but do not remove readiness checks that prevent incomplete images. If you process many URLs, bound concurrency to the capacity and cost profile of your Lambda configuration.
Reliability
Use explicit timeouts for navigation and selector waits, validate input URLs, and return clear failure responses for invalid input and capture errors. Consider whether your caller may retry an invocation: if it does, make storage keys or downstream writes safe for retries. Pages outside your control can change, return bot checks, or fail intermittently, so treat a successful browser launch as separate from a successful, useful capture.
Cost
Lambda cost depends on invocation count, configured memory, execution duration, and associated storage or data transfer. Browser launch and wait time contribute to duration; memory settings also affect CPU allocation. The research facts here do not include a price estimate, so calculate cost using your account’s current AWS pricing and measured execution profile. Returning or storing images may also affect downstream service and storage costs.
Or skip the browser setup
If you need a screenshot without packaging and maintaining Chromium in Lambda, ScreenshotNeo is a website screenshot API and MCP server. One GET request returns an image or PDF. Its API and configuration are documented at ScreenshotNeo docs.
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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(({ writeFile }) => writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));
- Cookie banners, newsletter popups, and chat widgets are removed before the shot; each cleanup step can be turned off.
- Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed. Responses identify the page verdict and billing status in headers.
- An MCP server provides
take_screenshot,get_page_info, andcapture_pdftools for Claude, Cursor, and other MCP clients. - 1,000 screenshots per month are free with no card. Paid plans start at $5 for 3,000 screenshots; all features are on every plan.
Create a free ScreenshotNeo account to get 1,000 screenshots a month with no card.
FAQ
Does fullPage: true include content below the viewport?
It requests a full-page capture, but content that has not rendered or has not been loaded by the page may still be absent. Trigger lazy loading and wait for the content your use case requires.
Can I use this handler with any Lambda trigger?
The browser capture pattern is independent of a particular trigger, but the response format and payload limits are not. Adapt the returned bytes or storage reference to the trigger and gateway you use.
Should I use ZIP or a container image?
Use the packaging route that fits the browser artifact size and your build workflow. ZIP deployments have documented package ceilings; containers allow a larger uncompressed image, up to the Lambda quota.


