Fix Puppeteer “out of memory” on AWS Lambda
The symptoms vary — Runtime exited with error: signal: killed,
Protocol error (Target.createTarget): Target closed, a REPORT line
showing Max Memory Used equal to your memory limit — but the
cause is the same: Chromium wants more memory than your function has.
Here is the checklist that actually fixes it, in order of impact.
1. Give the function 2048 MB
Chromium plus one rendering page peaks well past 1 GB on real HTML (webfonts, images, big DOM). Lambda also allocates CPU in proportion to memory, so a 2048 MB function renders roughly twice as fast as a 1024 MB one — at ~2× the per-ms rate, the render often costs about the same and times out far less.
# serverless.yml (equivalent settings apply to SAM/CDK/Terraform)
functions:
renderPdf:
handler: handler.handler
memorySize: 2048 # CPU scales with memory; 1024MB is the practical floor
timeout: 30
ephemeralStorageSize: 1024 # /tmp — Chromium extracts + scratch space
2. Use @sparticuz/chromium, not the stock binary
The npm puppeteer package downloads a 170–280 MB
Chromium that blows the 250 MB unzipped deployment limit and expects
libraries Amazon Linux doesn’t ship.
@sparticuz/chromium (the maintained successor to
chrome-aws-lambda) is built for Lambda: brotli-compressed to
fit, extracts to /tmp, and its chromium.args
include the flags that matter —
--disable-dev-shm-usage (Lambda has no /dev/shm),
--no-sandbox, --single-process.
npm install puppeteer-core @sparticuz/chromium
# Match major versions: each @sparticuz/chromium release targets a specific
# Chromium, and puppeteer-core pins the Chromium it speaks to. Check the
# compatibility table in the @sparticuz/chromium README when upgrading either.
3. Reuse the browser, never the page
Launching Chromium is the memory spike. Keep one browser in module scope for
the container’s lifetime; open a fresh page per invocation and close it in
finally. A page that leaks on the error path is a renderer
process that never dies — that’s
the classic Puppeteer memory leak.
// handler.mjs — Node.js 20.x Lambda, 2048MB memory, 30s timeout
import chromium from '@sparticuz/chromium';
import puppeteer from 'puppeteer-core';
// Module scope: reused across warm invocations, so we pay the ~2s launch
// (and its memory spike) once per container, not once per request.
let browser;
async function getBrowser() {
if (browser && browser.connected) return browser;
browser = await puppeteer.launch({
args: chromium.args, // includes --no-sandbox, --disable-dev-shm-usage, --single-process
defaultViewport: chromium.defaultViewport,
executablePath: await chromium.executablePath(),
headless: 'shell',
});
return browser;
}
export const handler = async (event) => {
const { html } = JSON.parse(event.body ?? '{}');
const b = await getBrowser();
const page = await b.newPage();
try {
// Block leftovers that eat memory for nothing in a PDF.
await page.setRequestInterception(true);
page.on('request', (req) =>
['media', 'websocket'].includes(req.resourceType()) ? req.abort() : req.continue()
);
await page.setContent(html, { waitUntil: 'networkidle0', timeout: 25_000 });
const pdf = await page.pdf({ format: 'A4', printBackground: true });
return {
statusCode: 200,
isBase64Encoded: true,
headers: { 'Content-Type': 'application/pdf' },
body: Buffer.from(pdf).toString('base64'),
};
} finally {
// ALWAYS close the page — every leaked page is a leaked renderer process.
await page.close().catch(() => {});
}
};
4. Render one page per invocation
Don’t Promise.all five renders inside one function — each
concurrent page is another renderer process (~a CPU core and hundreds of MB).
Lambda’s unit of scale is the invocation; let concurrency happen there.
5. Still stuck? The remaining 10%
- Huge documents: a 200-page report can exhaust 2048 MB during PDF
serialization. Raise
ephemeralStorageSizeand memory (up to 10,240 MB), or split the document. - Layer squeeze: if the function bundle is fighting the 250 MB limit, switch to a container image (10 GB limit) — same code, saner packaging.
- Security note:
--no-sandboxis unavoidable on Lambda. Only render HTML you trust or have sanitized.
FAQ
How much memory does Puppeteer need on Lambda?
Plan for 1600–2048MB. Chromium plus one rendering page routinely peaks past 1GB on real-world HTML, and Lambda allocates CPU proportionally to memory, so 2048MB also renders roughly twice as fast as 1024MB. Below 1024MB, OOM kills are a matter of time.
Why does plain `puppeteer` not work on Lambda?
The npm puppeteer package downloads a full Chromium build (~170–280MB unpacked) that exceeds the 250MB unzipped deployment limit and expects shared libraries Amazon Linux does not ship. Use puppeteer-core plus @sparticuz/chromium, a Lambda-specific Chromium build that is brotli-compressed to fit and ships the right launch flags.
What does --disable-dev-shm-usage do?
Chromium normally writes shared memory to /dev/shm, which does not exist on Lambda (and defaults to only 64MB in Docker). The flag makes Chromium use /tmp instead. @sparticuz/chromium includes it by default; add it yourself anywhere else you run headless Chrome.
Is it safe to reuse the browser across invocations?
Yes, and you should — launching Chromium is the slowest, most memory-spiky part of the invocation. Keep the browser in module scope, check it is still connected before reuse, and always close the page (not the browser) in a finally block.
…or skip all of this with one API call.
NippyPDF is a managed Chromium rendering core behind a single
POST /v1/pdf — no browser fleet, no page-break roulette,
around $0.003/document.
Try the live demo Join the waitlist Why teams stop self-hosting Puppeteer →