How To Use Puppeteer on AWS Lambda for Headless Automation

Puppeteer on AWS Lambda is a powerful combination for headless browser automation, with no dedicated servers, automatic scaling, and pay-per-execution billing. This guide walks you through the correct setup, working code, and the config mistakes that break most first attempts.
What is Puppeteer?
Puppeteer is a Node.js library that controls headless Chromium via the Chrome DevTools Protocol. Unlike simple HTTP scrapers, it runs a real browser, handling JavaScript-rendered pages, user interactions, and DOM events.
Common use cases:
- Screenshots and PDF generation
- Web scraping of JavaScript-heavy pages
- Automated UI testing
- Pre-rendering SPAs for SEO
Why Run AWS Lambda Puppeteer for Headless Automation?
Lambda is event-driven and serverless; it spins up on demand, scales automatically, and charges only for execution time. For bursty automation tasks (screenshot APIs, scheduled scrapes, on-demand PDF generation), it's far more cost-effective than a running EC2 instance.
The challenge is that Lambda has strict constraints: a 250MB unzipped package limit, a default 3-second timeout, and no display server. Getting Puppeteer running correctly requires a few specific decisions.
The Right Package Combination
Most setups break here first.
Don't use the standard puppeteer package, it bundles a full Chromium binary (~170MB) and will exceed Lambda's size limit immediately.
Use instead:
npm install puppeteer-core @sparticuz/chromium- puppeteer-core - the full Puppeteer API without a bundled browser
- @sparticuz/chromium - a Lambda-optimized Chromium binary (~50MB compressed) maintained specifically for serverless environments. See the project on GitHub
This combination stays within Lambda's limits and provides the correct args and executable path for Lambda's runtime.
Setting Up Puppeteer Lambda with Layers
Lambda Layers let you separate heavy dependencies from your function code. Upload Chromium once, attach it to as many functions as you need.
Step 1: Create the Node.js Layer
mkdir nodejs && cd nodejs
npm init -y
npm install puppeteer-core @sparticuz/chromium
cd ..
zip -r puppeteer-layer.zip nodejs/
The nodejs/ folder structure is required — Lambda expects this exact path for Node.js layers.Step 2: Upload the Layer
Go to Lambda → Layers → Create layer, upload puppeteer-layer.zip, and select Node.js 18.x as the compatible runtime.
Partner with Us for Success
Experience seamless collaboration and exceptional results.
Step 3: Add the Sparticuz Chromium Layer
Add the public Sparticuz Chromium layer by ARN. For ap-south-1:
arn:aws:lambda:ap-south-1:764866452798:layer:chrome-aws-lambda:46
For other regions and the latest version, check Sparticuz/chromium releases.
Step 4: Attach Both Layers to Your Function
In your Lambda function, go to Configuration → Layers → Add a layer and attach both:
- The Sparticuz Chromium layer (by ARN)
- Your custom Node.js layer (from Step 2)
Step 5: Configure Memory and Timeout
This is where most setups fail silently.
Go to Configuration → General configuration and set:
| Setting | Minimum | Recommended |
Memory | 1024 MB | 2048–3072 MB |
Timeout | 15 sec | 30–60 sec |
Lambda allocates CPU proportional to memory; more memory means faster browser startup. The default 3-second timeout won't survive a single Chromium launch.
Lambda Handler: Screenshot
import chromium from '@sparticuz/chromium';
import puppeteer from 'puppeteer-core';
export const handler = async (event) => {
let browser = null;
try {
browser = await puppeteer.launch({
args: chromium.args,
defaultViewport: chromium.defaultViewport,
executablePath: await chromium.executablePath(
'/opt/nodejs/node_modules/@sparticuz/chromium/bin'
),
headless: chromium.headless,
ignoreHTTPSErrors: true,
});
const page = await browser.newPage();
await page.goto(event.url || 'https://example.com', {
waitUntil: 'networkidle2',
});
const screenshot = await page.screenshot({ encoding: 'base64' });
return {
statusCode: 200,
headers: { 'Content-Type': 'image/png' },
body: screenshot,
isBase64Encoded: true,
};
} catch (error) {
console.error(error);
return { statusCode: 500, body: JSON.stringify({ error: error.message }) };
} finally {
if (browser) await browser.close(); // Always close — prevents memory leaks
}
};Lambda Handler: PDF Generation
import chromium from '@sparticuz/chromium';
import puppeteer from 'puppeteer-core';
export const handler = async (event) => {
let browser = null;
try {
browser = await puppeteer.launch({
args: chromium.args,
executablePath: await chromium.executablePath(
'/opt/nodejs/node_modules/@sparticuz/chromium/bin'
),
headless: chromium.headless,
});
const page = await browser.newPage();
await page.goto(event.url, { waitUntil: 'networkidle0' });
const pdf = await page.pdf({
format: 'A4',
printBackground: true,
});
return {
statusCode: 200,
headers: { 'Content-Type': 'application/pdf' },
body: pdf.toString('base64'),
isBase64Encoded: true,
};
} finally {
if (browser) await browser.close();
}
};Common Errors and Fixes
Error: Failed to launch the browser process. Memory is too low, or executablePath is wrong. Set memory to ≥ 2048 MB and verify the path points to /opt/nodejs/node_modules/@sparticuz/chromium/bin.
Task timed out after 3.00 seconds. Increase the Lambda timeout to at least 30 seconds under Configuration → General configuration.
Cannot find module 'puppeteer-core'.Your layer ZIP is missing the nodejs/node_modules/ folder structure. Repackage with nodejs/ as the root directory.
spawn /opt/chromium ENOENT. The Sparticuz Chromium layer ARN is missing or for the wrong region. Verify both layers are attached.
Pages loading blank or partially. Adjust waitUntil: use networkidle2 for most pages, domcontentloaded for fast scraping, networkidle0 for heavy single-page apps.
Partner with Us for Success
Experience seamless collaboration and exceptional results.
Alternative: Container Image Deployment
If your use case requires custom system dependencies or you're already containerizing Lambda functions, the container image approach sidesteps the 250MB limit entirely (10GB max).
FROM public.ecr.aws/lambda/nodejs:18
RUN dnf install -y chromium
COPY index.mjs ${LAMBDA_TASK_ROOT}/
CMD ["index.handler"]Set executablePath to /usr/bin/chromium-browser. Pre-installed Chromium also means no extraction overhead at startup, which improves cold start times.
Frequently Asked Questions
Why use puppeteer-core instead of puppeteer on Lambda?
The standard puppeteer package bundles its own Chromium (~170MB), which exceeds Lambda's 250MB limit. puppeteer-core is the same API without the browser — you supply the executable via a layer.
What is @sparticuz/chromium?
A community-maintained, serverless-optimized Chromium binary built for Lambda. It exports the correct args, executablePath, and headless values for Lambda's execution environment.
Can Lambda Puppeteer handle concurrent requests?
Yes, each Lambda invocation runs in its own isolated container, so 100 concurrent requests spin up 100 browser instances in parallel. Always call browser.close() in a finally block.
Is Puppeteer on Lambda suitable for long-running crawls?
No. Lambda's 15-minute execution limit and per-duration cost make it a poor fit for sustained scraping. Use it for short, on-demand tasks. For long crawls, use ECS Fargate or a dedicated scraping platform.
Final Thoughts
The key decisions that make Puppeteer Lambda work: use puppeteer-core + @sparticuz/chromium, set memory to at least 2048 MB, extend the timeout to 30+ seconds, and always close the browser in a finally block.
Get these right, and you have a scalable, serverless, headless browser that handles bursts automatically, no server management, no Chromium installation headaches.



