Fix Puppeteer “out of memory” on AWS Lambda

Updated July 2026 · applies to puppeteer-core 22+ and @sparticuz/chromium

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%

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 demoJoin the waitlistWhy teams stop self-hosting Puppeteer →