Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.
Short answer: you can render a PDF in Lambda by launching a Lambda-compatible Chromium binary from Node.js, loading HTML with Puppeteer (or a compatible browser library), and calling page.pdf(). However, nodejs18.x is now a legacy runtime: AWS lists September 1, 2025 as its deprecation date, February 1, 2027 as the date it blocks new function creation, and March 3, 2027 as the date it blocks updates. For a new function, choose a currently supported Node.js runtime and verify that your exact Chromium distribution and browser library support it. Use Node.js 18 only when you are maintaining an existing deployment or have a documented compatibility requirement.
What the Lambda PDF architecture looks like
A typical request-to-PDF flow has six parts:
- Lambda receives HTML, a URL, or data used to build a document.
- Your handler starts a Chromium process with Lambda-safe flags.
- The browser opens the page and waits for the content, fonts, images, and other assets you require.
- The handler calls
page.pdf()with paper, margin, orientation, and background settings. - The resulting bytes are returned directly or written to storage such as Amazon S3.
- The browser is closed and temporary files are removed or reused safely.
The browser binary, native libraries, and Node dependencies must all be present in the deployment artifact. A normal desktop Puppeteer install is not automatically suitable for Lambda.
Choose the runtime before choosing Chromium
AWS lists nodejs18.x on Amazon Linux 2 as deprecated. Its published lifecycle dates are September 1, 2025 for deprecation, February 1, 2027 for blocking function creation, and March 3, 2027 for blocking function updates. Check the AWS Lambda runtimes table immediately before deployment because lifecycle dates can change.
Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchWindows Errors? Fix Them Before They Spread
Repair common Windows errors and clear accumulated junk for a smoother, more stable PC - no reinstall needed.Free scan · no reinstallFor a new function, select a currently supported Node.js runtime, then verify all of these against the browser package you intend to use:
#1 Best Overall
- Supported Node.js major version and Amazon Linux base.
x86_64orarm64support.- Chromium revision and its required shared libraries.
- Whether the package expects a ZIP layer, a container image, or a particular executable path.
- Launch arguments required in a sandboxed Lambda environment.
No single Puppeteer/Chromium pairing is universally safe. Pin a named package and version, inspect its documentation, and test the final artifact on the same runtime and architecture as production. Treat the code below as an integration pattern, not proof that an unselected package works.
ZIP or container image?
Lambda supports both deployment styles. Compare the final artifact, not just your application source.
| Deployment | Limits and implications | Best fit |
|---|---|---|
| ZIP | 50 MB zipped for direct API or SDK upload; 250 MB maximum unzipped combined contents, including applicable layers. Larger ZIPs can be uploaded through Amazon S3, but the 250 MB unzipped ceiling remains. | A compact, well-understood dependency tree that fits after building for Lambda. |
| Container image | AWS lists a 10 GB maximum uncompressed image size. You control the OS libraries and browser layout, while AWS base images include the Lambda runtime interface components. | Chromium and native libraries make a ZIP awkward, or you need a reproducible image build. |
AWS describes three ways to build a Node.js Lambda container image: an AWS Node.js base image, an AWS OS-only base image, or a non-AWS base image. Whichever route you choose, build for the target architecture and inspect the image or archive to confirm that the executable and shared libraries are really present.
Do these 3 things before closing this tab:
1Clear out junk files and repair common Windows errors2Scan for outdated or missing drivers - takes under a minute3Repair Windows errors before they cause bigger problemsResource settings that PDF rendering actually needs
Lambda allows 128 MB through 10,240 MB of memory and a maximum standard timeout of 900 seconds (15 minutes). These are service ceilings, not recommended PDF settings. More memory also provides more CPU. Measure cold starts, navigation time, font loading, and PDF generation with your largest realistic documents before selecting values.
Rank #2
/tmp is temporary storage unique to an execution environment. It defaults to 512 MB and can be configured from 512 MB through 10,240 MB. Chromium extraction, its cache, downloaded assets, and an output PDF can consume that space. Increase ephemeral storage when measurements require it, and delete temporary files you no longer need.
- Start with a timeout long enough for cold start plus navigation and PDF generation; do not rely on the 15-minute maximum as a target.
- Test pages with remote images, web fonts, large tables, and slow APIs.
- Limit concurrency or queue jobs if many browsers can exhaust account, memory, or downstream-service capacity.
- Reuse a browser only when you can isolate pages and reliably detect a crashed process; otherwise launch and close per invocation for simpler failure handling.
Build a Node.js Lambda handler
The following handler shows the API shape. Replace the imports and executable-path logic with the exact, version-pinned Chromium distribution you have verified for your runtime and architecture. Do not copy a desktop Chromium binary into a ZIP and assume it will run.
import puppeteer from 'YOUR_VERIFIED_PUPPETEER_PACKAGE';
import chromium from 'YOUR_VERIFIED_LAMBDA_CHROMIUM_PACKAGE';
export const handler = async (event) => {
const html = event.html ?? '<!doctype html><html><body><h1>Hello PDF</h1></body></html>';
let browser;
try {
browser = await puppeteer.launch({
args: chromium.args,
defaultViewport: { width: 1280, height: 900, deviceScaleFactor: 1 },
executablePath: await chromium.executablePath(),
headless: true
});
const page = await browser.newPage();
await page.setContent(html, { waitUntil: 'networkidle0' });
await page.emulateMediaType('print');
const pdf = await page.pdf({
format: 'A4',
printBackground: true,
margin: { top: '16mm', right: '14mm', bottom: '16mm', left: '14mm' },
preferCSSPageSize: true
});
return {
statusCode: 200,
headers: { 'Content-Type': 'application/pdf', 'Content-Disposition': 'inline; filename="document.pdf"' },
isBase64Encoded: true,
body: Buffer.from(pdf).toString('base64')
};
} finally {
if (browser) await browser.close();
}
};
The placeholder package names are deliberate: the correct module, version, executable path, and launch arguments depend on the distribution you select. Before publishing an implementation, build it and run it in a Lambda-like environment, then verify that a real PDF opens and contains expected text, images, fonts, and page breaks.
Control HTML, CSS, and page layout
Use print CSS
Set page dimensions and breaks in CSS when possible:
@page { size: A4; margin: 16mm 14mm; }
@media print {
.avoid-break { break-inside: avoid; }
.page-break { break-before: page; }
}
body { font-family: "Inter", Arial, sans-serif; }
preferCSSPageSize: true lets a valid @page rule control the paper size. If you omit it, the format option in the handler is the fallback.
Wait for assets explicitly
networkidle0 can wait indefinitely on pages that keep analytics or streaming connections open. For controlled HTML, wait for a known selector instead:
await page.setContent(html, { waitUntil: 'domcontentloaded' });
await page.waitForSelector('#report-ready', { timeout: 30000 });
await page.evaluate(() => document.fonts.ready);
For a URL rather than supplied HTML, use page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 }), then wait for a selector or an application-specific readiness signal. Ensure the Lambda function has outbound network access and that private resources are reachable from its VPC configuration.
Fonts, images, and security
- Bundle fonts when deterministic output matters; remote font requests can fail or change layout.
- Use absolute, reachable asset URLs or inline small assets as data URLs.
- Validate or sanitize user-provided HTML. A browser can execute scripts and make requests using the function’s network access.
- Do not expose arbitrary URL fetching without allowlists, authentication controls, and request limits.
Packaging and deployment checklist
- Pin the Node.js runtime, architecture, browser package, and automation-library versions.
- Build dependencies for the Lambda operating system and architecture, not your laptop’s native environment.
- For ZIP, inspect the zipped size and the uncompressed combined size of function code and layers. Keep within 50 MB for direct upload and 250 MB unzipped.
- For a container, inspect the final uncompressed image and keep it below the 10 GB Lambda limit; remove development caches and unused browsers.
- Set memory, timeout, and ephemeral storage in the function configuration based on measurements.
- Invoke the deployed function with representative HTML and save the returned bytes as a PDF.
- Repeat tests after every browser or base-image update, including cold starts and concurrent invocations.
AWS recommends including the SDK modules a function uses, together with dependencies, in the deployment package or a Lambda layer to control dependency versions and backward compatibility. Apply the same discipline to your browser stack.
Rank #4
Common failures and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
ENOENT or executable not found |
The browser was not packaged, or the path is wrong. | List files in the built artifact, use the package’s documented executable-path method, and verify architecture. |
| Browser exits immediately | Missing native library, incompatible binary, or incorrect sandbox flags. | Use a Lambda-supported distribution, build on a compatible base, and apply only the launch arguments documented by that package. |
| Task timed out | Slow navigation, never-ending connections, or insufficient timeout. | Use a readiness selector, set navigation timeouts, remove unnecessary third-party requests, and measure a higher timeout. |
| Blank or incomplete PDF | Rendering started before fonts, images, or client-side data finished. | Wait for a specific application-ready selector and document.fonts.ready; verify network access. |
| “No space left on device” | Chromium, cache, assets, or output exceeded /tmp. |
Increase ephemeral storage within 512 MB–10,240 MB, clean temporary files, and avoid retaining unbounded caches. |
| ZIP rejected for size | Compressed or uncompressed package exceeds the applicable quota. | Measure the built artifact, remove unused files, move to a layer where appropriate, or evaluate a container image. |
| Works locally but not in Lambda | Different OS libraries, architecture, fonts, environment variables, or network path. | Run integration tests in a matching Lambda base image or equivalent CI environment. |
Observability, reliability, and cost decisions
Log the selected browser version, architecture, navigation duration, PDF duration, output byte count, and remaining /tmp space. Avoid logging document contents or secrets. Emit structured errors that distinguish browser launch, navigation, readiness, PDF creation, and storage failures.
Lambda billing depends on invocation duration, memory allocation, and request volume; PDF-rendering performance and cost vary by workload, so no universal benchmark or cost figure applies. Benchmark your own page mix, including cold starts and retries. If a job can be retried, use an idempotency key so a timeout does not create duplicate records or files. Store large PDFs in object storage rather than returning oversized synchronous responses, and use an asynchronous queue for documents that approach your timeout budget.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server for developers. A single GET request returns PNG, JPEG, WebP, or PDF, so you do not package Chromium or maintain browser processes in Lambda. It removes cookie-consent banners, newsletter popups, and chat widgets before capture; bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients.
Quick wins for a faster PC:
Repair Windows errors before they cause bigger problemsFix Now →Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Clear out junk files and repair common Windows errorsFree Scan →For a PDF capture, see the ScreenshotNeo API documentation and call:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
The endpoint also accepts options for full-page capture, CSS selectors, dark mode, device presets, retina scale, PDF paper size and margins, custom CSS or JavaScript, click and wait actions, request blocking, headers, cookies, user agent, timezone, geolocation, transparent backgrounds, resizing, caching, signed links, asynchronous webhooks, bulk capture of up to 100 URLs per call, and usage reporting. Every feature is available on every plan. The free plan includes 1,000 shots per month without a card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account to get an API key.
Best Value
Node.js, Python, and cURL callers
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`ScreenshotNeo failed: ${res.status}`);
const file = Buffer.from(await res.arrayBuffer());
Python
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
Use the requested output and PDF parameters from the documentation when you need a PDF rather than the default image format.
FAQ
Can I create a new Lambda with Node.js 18?
AWS has scheduled creation and update blocks for the deprecated managed runtime. Use a supported runtime for new work and reserve Node.js 18 for an existing, compatibility-constrained deployment while you migrate.
Should I use ZIP or a container?
Choose after measuring the complete artifact and considering build reproducibility, native-library control, and your team’s operational experience. ZIP is simpler when it fits; a container is worth evaluating when Chromium dependencies make ZIP packaging difficult.
Where should the generated PDF go?
Return it for small synchronous responses or write it to object storage for larger, asynchronous, or retryable jobs. Select the delivery pattern based on PDF size, client latency, and durability requirements.
Frequently Asked Questions
Does Lambda include Chromium by default?
No. You must package a compatible browser binary and its native dependencies in the ZIP/layer or container image.
What is the maximum PDF render time?
The standard Lambda timeout ceiling is 900 seconds, but practical rendering time depends on your page, browser startup, network, memory, and concurrency.
Free tools Windows power users keep installed
One-click scans. No signup required.
Why does a PDF differ between local and Lambda?
Differences in fonts, operating-system libraries, architecture, browser revision, network access, and timing commonly change layout. Test in a Lambda-matching environment.
Quick Recap
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.

