Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

For small, predictable documents, Puppeteer can render a PDF inside AWS Lambda. For large or highly variable documents, the reliable design is an asynchronous S3-and-queue workflow feeding a containerized Chromium worker, such as ECS or Fargate. Lambda has a 900-second invocation ceiling, finite memory, package-size limits and a 6 MB synchronous response quota, so architecture and output delivery matter as much as the PDF code.

Choose the execution model before writing PDF code

Use Lambda when jobs are short enough to fit comfortably inside the timeout, the Chromium package fits the deployment limits, and bursty concurrency is more important than a long-running worker. Use an SQS-triggered ECS/Fargate worker when documents can approach 15 minutes, have unpredictable asset latency, require more memory or need stronger per-job isolation.

Concern Lambda ECS/Fargate worker
Maximum job duration 900 seconds per invocation Not bounded by the Lambda invocation limit; set the worker and queue timeouts for your job
Memory and CPU 128 MB to 10,240 MB; at 1,769 MB AWS assigns the equivalent of one vCPU Choose task CPU and memory independently for the Chromium workload
Chromium packaging Zip, layer or Lambda container, subject to 50 MB zipped and 250 MB unzipped package quotas Put Chromium and system libraries in the container image
Startup and scaling Fast for short bursts, but cold starts and concurrency can multiply browser launches Workers may take longer to start, but a warm pool and queue smooth large jobs
Output delivery Write to S3; a synchronous response is limited to 6 MB Write to S3 and return a job identifier or signed URL
Operations Less infrastructure to manage More control over browser version, isolation and long-running work

These are published AWS quotas; verify them against the current Lambda quotas for the Region and account you deploy.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Understand what Puppeteer is actually rendering

page.pdf() uses print media

Puppeteer’s page.pdf() returns PDF bytes and applies print CSS by default. If your site’s layout only works with screen styles, call await page.emulateMediaType('screen') before creating the PDF. The API and its options are documented in the Puppeteer page.pdf() reference.

Wait for application readiness, not just navigation

networkidle is not a universal definition of “ready”: analytics, advertisements or long polling can keep a page busy forever, while a page can become visually complete before every font or image has loaded. Make the page expose a readiness marker such as window.__PDF_READY__ = true, wait for that marker, then wait for document.fonts.ready and any required images.

Keep assets deterministic

  • Host CSS, images and fonts at URLs reachable from the deployed runtime.
  • Package fonts that are not available in the Lambda or container image.
  • Do not depend on files that exist only on a developer laptop.
  • If the function runs in a VPC, verify DNS, routing, security groups and outbound access to every asset origin.

Design the Lambda PDF path

  1. Package a compatible browser. Puppeteer documents the Lambda package-size problem and points to compatible Chromium distributions in its troubleshooting guide. A Lambda layer or container image can keep the browser binary separate from application code. On Amazon Linux EC2, Chromium requires EPEL and its system dependencies.
  2. Allocate memory from measurements. The 128 MB console default is rarely appropriate for a large Chromium render. Memory also controls CPU, so increasing it can reduce render time as well as prevent out-of-memory failures.
  3. Give each job one context and page. Reusing a browser process across warm invocations can reduce startup cost, but create and close a fresh context/page for each document so cookies, DOM state and retained buffers do not leak between jobs.
  4. Use a bounded sequence. Navigate, wait for your readiness signal, wait for fonts and assets, call page.pdf(), upload the result, and only then return from the handler.
  5. Store large output outside the response. Write to /tmp or stream the bytes to S3. Return a job ID or signed S3 URL instead of sending a multi-megabyte PDF through a synchronous Lambda response.

A Lambda handler that uploads the PDF to S3

The following CommonJS example expects a Lambda-compatible Chromium executable at CHROMIUM_PATH and uses puppeteer-core. Supply the executable and its dependencies through a layer or container image; do not assume a desktop Chromium binary will run in Lambda.

const puppeteer = require('puppeteer-core');
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');

const s3 = new S3Client({});
const bucket = process.env.OUTPUT_BUCKET;
const chromiumPath = process.env.CHROMIUM_PATH;

exports.handler = async (event) => {
  const url = event.url;
  const key = event.key || `pdf/${Date.now()}.pdf`;
  if (!url || !bucket || !chromiumPath) {
    throw new Error('url, OUTPUT_BUCKET and CHROMIUM_PATH are required');
  }

  let browser;
  let page;
  try {
    browser = await puppeteer.launch({
      executablePath: chromiumPath,
      headless: true,
      args: ['--no-sandbox', '--disable-dev-shm-usage']
    });
    const context = await browser.createBrowserContext();
    page = await context.newPage();
    page.setDefaultNavigationTimeout(90000);
    page.setDefaultTimeout(30000);

    await page.goto(url, { waitUntil: 'domcontentloaded' });
    await page.waitForFunction(() => window.__PDF_READY__ === true, {
      timeout: 60000
    });
    await page.evaluate(() => document.fonts.ready);

    // Use this only when the document requires screen CSS.
    // await page.emulateMediaType('screen');

    const pdf = await page.pdf({
      path: '/tmp/document.pdf',
      printBackground: true,
      preferCSSPageSize: true,
      margin: { top: '12mm', right: '12mm', bottom: '12mm', left: '12mm' }
    });

    await s3.send(new PutObjectCommand({
      Bucket: bucket,
      Key: key,
      Body: pdf,
      ContentType: 'application/pdf'
    }));

    return { status: 'complete', bucket, key };
  } finally {
    if (page) await page.close().catch(() => {});
    if (browser) await browser.close().catch(() => {});
  }
};

Replace the readiness contract with the signal used by your application. If some pages do not emit one, use a bounded selector wait or delay as a fallback rather than an unbounded sleep. The finally block is essential: AWS notes that warm environments retain globals and that callbacks finishing after the handler exits cause confusing behavior. Await navigation, font loading, PDF serialization, S3 upload and cleanup before returning.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

PDF options that affect large documents

Page size and CSS

Use format or explicit width and height when the product requires a fixed paper size. Set preferCSSPageSize only when your document defines matching @page rules; otherwise Puppeteer can scale content unexpectedly. Keep printBackground: true when colored panels, charts or background images are part of the document.

Margins and page breaks

Define margins once, preferably in CSS or the PDF options, and test the largest representative document. Use print-specific CSS such as break-before, break-after and break-inside to prevent headings, table rows or cards from splitting in unsuitable places.

Page ranges

pageRanges can limit output to selected pages, but it is safest after you have verified pagination on the complete document. A late-loading image or font can shift page numbers and make a previously correct range wrong.

Lambda limits that commonly break “large” PDFs

Limit Published value Design consequence
Memory 128 MB–10,240 MB Measure peak Chromium usage; increase memory to gain CPU and headroom
Standard timeout 900 seconds maximum Jobs that can approach 15 minutes need an asynchronous architecture
Deployment upload 50 MB zipped A full browser binary may not fit in a direct zip upload
Unzipped package 250 MB Use a layer or container when browser plus dependencies exceed this
Ephemeral storage 512 MB–10,240 MB in /tmp Allocate enough space for Chromium, temporary files and the largest PDF
Synchronous payload 6 MB request and response Do not return large PDF bytes through the synchronous API path

AWS states that “After the timeout value is reached, Lambda stops the function invocation.” Monitor Max Memory Used, duration, timeout counts and error logs; size the function from upper-bound documents, not the average file.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Move long or variable jobs to S3, SQS and ECS/Fargate

  1. The API accepts the document parameters and creates a job record with status queued.
  2. Input HTML, data or a source URL is stored in S3. The API sends only the bucket/key and rendering options to SQS.
  3. An ECS/Fargate worker pulls one message, launches its containerized Chromium, renders the PDF and uploads it to an output S3 key.
  4. The worker marks the job complete or failed, records a diagnostic message and deletes the queue message only after durable output exists.
  5. The client polls a status endpoint or receives a notification, then downloads the PDF through a signed S3 URL.

Set a visibility timeout longer than the worst-case render and upload time, add retries for transient network failures, and route repeatedly failing messages to a dead-letter queue. Limit worker concurrency so each task has enough memory for its browser; opening many pages inside one task can exhaust memory faster than adding workers.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Performance and reliability checklist

  • Measure the peak: test the longest HTML, highest image count and largest font set you will accept.
  • Reduce transfer cost: serve appropriately sized images, avoid unnecessary third-party scripts and block resources that cannot affect the PDF.
  • Control concurrency: one page per job is easier to reason about; parallel pages need measured memory headroom.
  • Bound every wait: navigation, selectors, readiness signals, uploads and queue visibility must all have finite timeouts.
  • Keep output streaming-friendly: avoid retaining multiple full PDF buffers or copies of large HTML strings.
  • Log identifiers: include job ID, URL, document size, elapsed stages, memory used and the final S3 key.
  • Test the deployed runtime: local Chrome success does not prove that Lambda’s libraries, fonts, networking or sandbox behave identically.

Troubleshooting common failures

Symptom Likely cause Fix
Browser fails to launch or reports missing shared libraries Desktop Chromium or incompatible system libraries Use a Lambda-compatible Chromium distribution or container. On Amazon Linux EC2, install EPEL and Chromium dependencies as described by Puppeteer’s troubleshooting documentation.
Task timed out or Status: timeout Asset latency, insufficient CPU or a job beyond Lambda’s hard limit Inspect CloudWatch logs, increase memory, reduce asset latency, raise timeout within 900 seconds, then move long jobs to an asynchronous container worker.
Out-of-memory error or browser disconnect Large DOM/PDF buffers, too many simultaneous pages or warm-process leakage Increase memory, reduce parallel pages, close contexts promptly and avoid retaining duplicate HTML/PDF buffers.
PDF is truncated or the front door returns 5xx The synchronous response exceeds the 6 MB quota or another API payload limit Upload to S3 and return a job ID or signed URL.
Fonts or images are missing Assets are unreachable, fonts are not installed or rendering starts too early Package required fonts, verify deployed-runtime URLs and wait for explicit readiness and document.fonts.ready.
Colors or pagination differ from the browser preview Print media is active by default, or page-break CSS was not tested Call emulateMediaType('screen') when screen CSS is required, then test print CSS, breaks and preferCSSPageSize.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server that can return PNG, JPEG, WebP or PDF from one GET request. It accepts cookie and consent banners like a visitor, then removes more than 60 known consent platforms, newsletter popups and chat widgets before capture; each cleanup step can be disabled. Only clean shots are billed: bot checks, CAPTCHAs, blank pages, timeouts, failed loads and cache hits are free, and every response identifies the page verdict and billing status in headers.

For a direct capture, use the documented endpoint (the endpoint also supports PDF output; configure the desired format in the 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}`);

ScreenshotNeo also provides an MCP server with take_screenshot, get_page_info and capture_pdf tools for Claude, Cursor and other MCP clients. It includes full-page capture with lazy images loaded, CSS-selector element capture, device and viewport controls, retina scale, custom CSS and JavaScript, click and wait actions, request blocking, headers and cookies, timezone and geolocation, resizing, chosen cache TTLs, signed image links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, a usage API and an OpenAPI specification. The free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Final deployment checklist

  • Run the largest supported document repeatedly in the deployed environment.
  • Confirm Chromium, fonts and all asset origins are available from the runtime.
  • Record peak memory and duration, then leave headroom for startup, serialization, upload and cleanup.
  • Verify that the API returns a job status or signed URL rather than oversized PDF bytes.
  • Exercise timeout, retry, duplicate-message and partial-upload paths before production traffic.

Frequently Asked Questions

What does preferCSSPageSize change?

When enabled, Puppeteer follows the document’s CSS @page size instead of forcing the PDF format. Use it only when those CSS rules are deliberate and tested.

When is pageRanges safe for a production export?

After pagination is deterministic. Late fonts, images or content can move page boundaries, so validate the complete document before relying on fixed page numbers.

Can I make a Lambda invocation run longer than 900 seconds?

No. 900 seconds is the published maximum standard Lambda timeout; jobs that may exceed it belong in an asynchronous container workflow.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.