Chromium is roughly 280MB. AWS Lambda's unzipped package limit is 250MB. If you've ever tried to npm install puppeteer and deploy straight to Lambda, you already know how that math works out (it doesn't).
I've spent enough time debugging "Failed to launch the browser process" errors at 2am to know this topic deserves a proper comparison, not another tutorial that shows you one method and leaves you guessing about the other two. So that's what this is: Layers vs. Container Images vs. direct ZIP upload, a version compatibility matrix that's actually current for 2026, and a troubleshooting section for the five errors you're statistically most likely to hit.
What Is Puppeteer on AWS Lambda (and Why Bother)
Puppeteer is a Node.js library that controls headless Chromium through the Chrome DevTools Protocol. Lambda is AWS's serverless compute — you pay per invocation, it scales automatically, and you never touch a server. Put them together and you get a browser automation setup that can fan out to hundreds of parallel executions without you provisioning a single EC2 instance.
The use cases are pretty consistent across teams: web scraping, screenshot and PDF generation, synthetic monitoring, pre-rendering single-page apps for SEO, and automated UI testing. The problem is always the same one I mentioned above — Chromium's size versus Lambda's package limits. That's why nobody deploys full puppeteer (which bundles its own Chromium download) to Lambda. Instead, you use puppeteer-core (no bundled browser) paired with a Lambda-optimized Chromium binary, most commonly @sparticuz/chromium.
That one substitution — puppeteer-core instead of puppeteer — solves 80% of the size problem before you've written a line of deployment config.
Layers vs. Container Image vs. ZIP: Pick Your Path First
Here's something that bugged me while researching this: nearly every existing guide covers exactly one deployment method. The AWS SAM tutorials use Layers. The CDK examples use Docker. A random Substack post uses a raw ZIP with a Chromium binary hosted on S3. Nobody puts them side by side, which means the first decision you actually need to make — which deployment method fits my situation — gets skipped entirely.
So let's fix that.
| Criteria | Lambda Layers | Container Image (Docker) | Direct ZIP Upload |
|---|---|---|---|
| Max package size | 250 MB unzipped (across all layers) | 10 GB image | 250 MB unzipped |
| Deployment complexity | Medium (layer ARN management) | Higher (Dockerfile + ECR push) | Lowest (zip & upload) |
| Cold start impact | Moderate | Slightly higher (larger image pull) | Moderate |
| Chromium update workflow | Re-publish layer version | Rebuild image | Re-upload zip |
| Best for | Quick prototypes, Serverless Framework users | Production workloads, teams with Docker CI | Simple one-off functions |
| IaC support | SAM, Serverless Framework | CDK, SAM, Terraform | Console, any IaC |
Both the 250MB and 10GB limits come straight from AWS's own Lambda quotas documentation — this isn't a number that's changed much, but it's the one constraint that decides your entire deployment strategy upfront.
My heuristic, for what it's worth: if you're prototyping or you're already using Serverless Framework, start with Layers. If you're shipping to production and your team already has Docker CI/CD, go Container Image — the 10GB ceiling gives you room to breathe. If you just need one function to take screenshots occasionally, direct ZIP is the least amount of ceremony.
All three paths use the same core dependency pairing underneath: puppeteer-core + @sparticuz/chromium. The deployment method changes how you package that pairing, not what you're packaging.

The 2026 Version Compatibility Matrix (Stop Guessing)
This is the part that actually costs people months, not hours. The single loudest complaint across Stack Overflow and GitHub issues isn't "how do I deploy this" — it's "why did my working deployment silently break after an npm update." The culprit is almost always a mismatch between @sparticuz/chromium, puppeteer-core, and the Node.js runtime.
First, the important callout: chrome-aws-lambda (the original alixaxel package) is deprecated. It's broken on Node 18+ and hasn't kept pace with Chromium releases. If you find a tutorial referencing it, close the tab — you're reading something outdated. Every current guide should point you to @sparticuz/chromium instead.
Use this compatibility rule instead of matching package majors by eye:
| Component | Version rule | What to verify before deployment |
|---|---|---|
puppeteer-core | Pick the Puppeteer version your application needs | Look up the Chromium build supported by that Puppeteer release |
@sparticuz/chromium | Its major version tracks the Chromium major, not the Puppeteer major | Match it to the Chromium build from Puppeteer's support table and read the Sparticuz release notes |
| AWS Lambda Node.js runtime | Use a currently supported Lambda runtime | Run an invocation test after every runtime or package update |
| Architecture | The npm package includes x64 binaries; arm64 support starts with Chromium v135 through an arm64 layer or remote pack | Match the Lambda architecture, layer/pack artifact, and Chromium version exactly |
I'm deliberately not hardcoding a package pair here because @sparticuz/chromium follows Chromium's release cycle and does not use normal semantic versioning. Start with the official Puppeteer Chromium Support page, note the Chromium major supported by your chosen Puppeteer release, and then select that major of @sparticuz/chromium. Finally, read the Sparticuz release notes for patch-level breaking changes and architecture details. Do not install the same major number for both packages unless that mapping happens to be confirmed by those two sources.

How to Deploy Puppeteer on AWS Lambda with Lambda Layers
A Lambda Layer lets you package Chromium separately from your function code, which keeps your actual handler small and lets you reuse the same Chromium layer across multiple functions. It's the closest thing to a "quick start" in this whole space.
Step 1: Install puppeteer-core and the -min package
When the Chromium files live in a Lambda Layer, keep the function package small by using @sparticuz/chromium-min. Replace the placeholders with the compatible versions you confirmed above:
npm install puppeteer-core@$PUPPETEER_VERSION \
@sparticuz/chromium-min@$CHROMIUM_VERSION
You're installing puppeteer-core — not puppeteer — because it skips the automatic browser download. The -min package supplies the launch helpers, while the layer supplies the Brotli-compressed Chromium files under /opt/chromium.
Step 2: Create or Reference a Chromium Lambda Layer
Use the architecture-specific layer archive attached to an official Sparticuz release, or build the archive from the official repository. For x86_64 Lambda, the documented build is:
git clone --depth=1 https://github.com/sparticuz/chromium.git
cd chromium
make chromium.x64.zip
That produces chromium.x64.zip. Upload it to S3 and publish it as a Lambda Layer with the runtime and architecture you actually use. For arm64, use the matching arm64 release artifact or build target; do not attach an x64 archive to an arm64 function.
If you're using SAM, attach the layer ARN directly in your template.yaml:
Resources:
PuppeteerFunction:
Type: AWS::Serverless::Function
Properties:
Layers:
- arn:aws:lambda:us-east-1:XXXXXXXXXXXX:layer:chromium-layer:1
Step 3: Write the Lambda Handler
Here's a working handler pattern that navigates to a URL and returns the page title:
import puppeteer from "puppeteer-core";
import chromium from "@sparticuz/chromium-min";
export const handler = async () => {
const browser = await puppeteer.launch({
args: puppeteer.defaultArgs({ args: chromium.args, headless: "shell" }),
executablePath: await chromium.executablePath("/opt/chromium"),
headless: "shell",
});
try {
const page = await browser.newPage();
await page.goto("https://example.com", { waitUntil: "domcontentloaded" });
return { title: await page.title() };
} finally {
await browser.close();
}
};
Notice the finally block. Always close the browser there — if you don't, warm Lambda environments accumulate zombie browser processes across invocations, and you'll eventually hit weird memory errors that have nothing to do with your actual code.
Step 4: Configure Memory, Timeout, and Architecture
Set memory to at least 1024 MB — I'd recommend 1536–2048 MB for anything beyond a trivial screenshot task. Set the timeout to at least 60 seconds. Pin your architecture to x86_64 unless you've specifically confirmed arm64 support for your exact Chromium version (this varies release to release).
Step 5: Deploy and Test
sam build && sam deploy --guided
Invoke it with a test event, then check CloudWatch Logs immediately if anything goes sideways — 90% of the errors in the troubleshooting section below show up clearly in those logs.
How to Deploy Puppeteer on AWS Lambda with Container Images (Docker)
Container images solve the 250MB headache entirely by giving you a 10GB ceiling instead. This is generally the better call for production workloads, especially if your team already has Docker in its CI pipeline.
Step 1: Create the Dockerfile
Start from an official AWS Lambda base image for Node.js, install your dependencies, and set the handler:
FROM public.ecr.aws/lambda/nodejs:20
COPY package*.json ./
RUN npm install --production
COPY . .
CMD ["index.handler"]
Depending on your Chromium package, you may need to yum install a few shared libraries (more on this in the troubleshooting section) — @sparticuz/chromium bundles most of what it needs, which reduces this friction considerably compared to installing full Chrome manually.
Step 2: Build and Push to Amazon ECR
aws ecr create-repository --repository-name puppeteer-lambda
docker build -t puppeteer-lambda .
docker tag puppeteer-lambda:latest <account-id>.dkr.ecr.<region>.amazonaws.com/puppeteer-lambda:latest
aws ecr get-login-password | docker login --username AWS --password-stdin <account-id>.dkr.ecr.<region>.amazonaws.com
docker push <account-id>.dkr.ecr.<region>.amazonaws.com/puppeteer-lambda:latest
Keep the image in the same region as your Lambda function — cross-region image pulls add latency you don't need.
Step 3: Create the Lambda Function from the Container Image
Point your function at the ECR image URI via CLI or CDK, and set memory (1536–2048 MB) and timeout (60–120 seconds) the same way you would with a layer-based deployment.
Step 4: Deploy and Test
Invoke with a test event and verify the output. The main tradeoff versus Layers: slightly higher cold starts due to the larger image pull, but you get way more headroom for dependencies.
How to Deploy Puppeteer on AWS Lambda with Direct ZIP Upload
This is the no-frills option — no layers to manage, no Docker to build. Good for prototypes or a single function that doesn't need to scale into a whole browser automation platform.
Step 1: Install Dependencies Locally
For a self-contained ZIP, use puppeteer-core + @sparticuz/chromium and keep both versions pinned. The full package contains the compressed Chromium files and extracts them to /tmp at runtime. Use @sparticuz/chromium-min only when those files are supplied separately through a Lambda Layer or a fast remote pack URL; the -min package does not include the Brotli files itself.
Step 2: Bundle and ZIP the Function
npm install --production
zip -r function.zip . -x "*.git*"
The --production flag matters here — dev dependencies eat into your 250MB budget for no reason.
Step 3: Upload and Configure the Lambda Function
aws lambda update-function-code --function-name my-puppeteer-fn --zip-file fileb://function.zip
If your ZIP exceeds 50MB, you can't upload it directly through the console or a simple CLI call — you'll need to upload to S3 first and reference the S3 URI instead. Set memory, timeout, and architecture the same as the previous two methods.
Step 4: Deploy and Test
Use the same invoke-and-check-logs flow. With the full package, chromium.executablePath() needs no argument. With chromium-min, pass the exact layer directory or remote pack URL, for example chromium.executablePath("/opt/chromium") for the layer layout above. A remote pack adds download work to the first cold start, so host it close to the function and verify the artifact version and architecture.
The puppeteer.launch() Args That Actually Work on Lambda
This is the snippet everyone copy-pastes, so let's get it right. Lambda's execution environment doesn't have /dev/shm, doesn't give you GPU access, and runs with restricted permissions — which means the default puppeteer.launch() call that works fine on your laptop will just... not work here.
const viewport = {
width: 1920,
height: 1080,
deviceScaleFactor: 1,
isMobile: false,
hasTouch: false,
isLandscape: true,
};
const browser = await puppeteer.launch({
args: await puppeteer.defaultArgs({ args: chromium.args, headless: "shell" }),
executablePath: await chromium.executablePath(),
headless: "shell",
defaultViewport: viewport,
});
The chromium.args array from @sparticuz/chromium already bakes in the flags that matter for a serverless environment — --no-sandbox, --disable-gpu, --disable-dev-shm-usage, and similar. That's the whole point of using the package instead of hand-rolling your own flag list: it tracks Chromium's requirements so you don't have to.

Troubleshooting: 5 Errors Every Developer Hits
No existing guide I found includes a proper troubleshooting section, which is a little baffling given that errors are almost certainly why you're reading this article in the first place.
"Failed to launch the browser process"
Root cause: missing shared libraries (libnss3.so, libatk, etc.) or an incorrect executablePath.
Fix: @sparticuz/chromium bundles most required dependencies, which is why it's the recommended package over rolling your own Chromium binary. For Docker deployments, if you're still hitting this, yum install the missing libs explicitly in your Dockerfile.
"Unzipped size must be smaller than 262144000 bytes"
Root cause: you installed the full puppeteer package, which bundles its own Chromium download (~400MB).
Fix: switch to puppeteer-core + @sparticuz/chromium. If you genuinely need more room, move to the Container Image approach and its 10GB ceiling.
"Browser disconnected" or Timeout on browser.newPage()
Root cause: insufficient Lambda memory, or you're missing flags like --disable-gpu in your launch args.
Fix: set memory to at least 1024MB (I'd go higher — see the benchmarks below) and make sure you're passing chromium.args rather than a stripped-down custom list.
Working Code Breaks After a Lambda Runtime Update
Root cause: AWS periodically patches the underlying runtime, which can shift shared library versions or Node.js patch versions out from under you.
Fix: pin your @sparticuz/chromium version explicitly, pin your Node runtime version in the function config, and — this is the part people skip — re-test after every AWS runtime announcement, not just when something breaks.
"Protocol error: Connection closed" After ~30 Seconds
Root cause: your Lambda timeout is shorter than the time it takes for the page to actually load and render.
Fix: bump the timeout to 60–120 seconds, set page.setDefaultNavigationTimeout() explicitly, and swap waitUntil: 'networkidle0' for waitUntil: 'domcontentloaded' if you don't need every last network request to settle before proceeding.
Production Hardening: Memory, Cold Starts, and Cost
Most guides tell you to "increase memory" and stop there. That's not actionable advice — here's what actually changes as you scale memory up.
Memory vs. Performance
Lambda allocates CPU proportionally to memory, which is the detail that trips people up. More memory isn't just "more RAM to work with" — it's also faster CPU, which directly speeds up Chromium's rendering. In practice, teams running Puppeteer benchmarks report meaningfully faster execution moving from 512MB up through the 1536–2048MB range, though your exact numbers will depend heavily on the pages you're rendering. Rather than quote a specific benchmark table that'll be stale by the time you read this, run your own test at 512MB, 1024MB, 1536MB, and 2048MB against your actual target pages — it's a ten-minute exercise that tells you exactly where your cost/performance sweet spot sits.
Provisioned Concurrency for Cold Starts
If you're running something latency-sensitive — synthetic monitoring, a real-time screenshot API — cold starts are your enemy. Provisioned Concurrency keeps a set number of execution environments warm and ready, eliminating the cold-start penalty at the cost of paying for that idle capacity. It's worth it specifically when latency matters more than raw cost efficiency.
arm64 (Graviton) for Cost Savings
Graviton-based Lambda functions run roughly 20% cheaper than x86_64 equivalents. The catch: @sparticuz/chromium arm64 support has historically been more limited than x86_64, so verify explicitly for your pinned version before committing to Graviton in production.
VPC vs. No-VPC
Placing your function in a VPC used to add meaningful cold-start latency; AWS has closed much of that gap in recent years, but it's still not zero. Only put your function in a VPC if it genuinely needs to reach private resources like RDS or ElastiCache — otherwise, skip it.
When to Move Off Lambda Entirely
If your browser tasks regularly exceed 15 minutes, need more than 10GB of memory, or require persistent browser sessions across requests, Lambda is fighting you at that point. ECS Fargate is built for exactly this — long-running, configurable-resource, pay-per-second compute. Lambda is fantastic for short, bursty, parallelizable browser tasks; it's the wrong tool once your workload starts looking like a persistent service.
When Deploying Puppeteer on Lambda Is the Wrong Approach
Here's something worth sitting with honestly: a large chunk of developers who land on "Puppeteer + Lambda" guides are actually trying to solve a data extraction problem, not a browser automation problem. If what you actually need is structured data from web pages — product listings, contact info, page content — all the Chromium packaging, version pinning, and layer management above is overhead you didn't need to take on.
Stick with Lambda + Puppeteer if you need genuine browser control: custom form interactions, screenshot/PDF pipelines, synthetic monitoring, or browser-based testing where you're actually manipulating the DOM programmatically.
Consider a scraping API if your end goal is structured JSON out of a webpage, not a browser session you're driving yourself. Thunderbit's Open API handles JS rendering, anti-bot measures, and CAPTCHAs behind a single HTTP call — POST /extract with a JSON Schema gets you structured data, POST /distill gets you clean Markdown. There's also an MCP server (thunderbit_extract, thunderbit_distill) if you're building an AI agent that needs to pull data mid-workflow without spinning up its own browser.
| Factor | Lambda + Puppeteer (DIY) | Extraction API (e.g., Thunderbit) |
|---|---|---|
| Setup time | Hours (packaging, layers, debugging) | Minutes (API key + HTTP call) |
| Maintenance | Ongoing (version pinning, runtime updates) | Handled by the provider |
| Anti-bot handling | Manual (stealth plugins, proxies) | Built-in |
| Output format | Raw HTML/screenshots you parse yourself | Structured JSON via schema |
| Best for | Full browser automation, testing, custom flows | Data extraction, scraping, content ingestion |
I'll put it plainly: if you're spending hours debugging Chromium binaries just to pull JSON out of product pages, that's a sign you're solving the wrong problem. Save the DIY Lambda route for when you genuinely need to drive a browser — for extraction, there's a more direct path. If you're weighing this tradeoff for a specific project, our guide to AI web scrapers walks through the landscape in more depth, and the Thunderbit Chrome Extension is worth a look if you want to test the extraction-first approach before committing to either path.
Wrapping Up
Three deployment methods, one recurring theme: pin your versions, give Chromium enough memory to breathe, and match your deployment method to your actual constraints rather than whatever tutorial you found first. Layers for quick iteration, Container Images for production scale, ZIP for the simple one-off. And if what you're really doing is data extraction rather than browser automation — it might be worth checking whether a purpose-built extraction API saves you the packaging headache entirely.
FAQs
Can you run Puppeteer on AWS Lambda in 2026?
Yes — using puppeteer-core paired with @sparticuz/chromium, deployed via Layers, Container Image, or direct ZIP. The full puppeteer package and the deprecated chrome-aws-lambda package no longer work reliably on current Lambda runtimes.
What's the maximum package size for AWS Lambda? 250MB unzipped for Layers and ZIP deployments; 10GB for Container Image deployments, per AWS's Lambda quotas.
Is chrome-aws-lambda still maintained?
No. The original chrome-aws-lambda package (by alixaxel) is deprecated and breaks on Node 18+. Use @sparticuz/chromium instead — it's the actively maintained standard at this point.
How much memory does Puppeteer need on AWS Lambda? 1024MB is the practical minimum; 1536–2048MB is where performance actually gets comfortable. Below 1024MB, expect noticeably slow execution since Lambda ties CPU allocation to memory.
How do I reduce cold start times for Puppeteer on Lambda? Allocate more memory (which also gets you more CPU), consider Provisioned Concurrency if latency is critical for your use case, and keep your deployment package as lean as possible — every extra dependency is extra cold-start time.\n


