Generating PDFs on Vercel works on the Node.js serverless runtime using playwright-core or puppeteer-core with @sparticuz/chromium, a Chromium build slim enough to fit the function bundle. It does not work on the Edge runtime, which has no native binary support, so Chromium cannot launch there at all. If you want zero browser bundling and a render call that works from any runtime including Edge, call a hosted API like PDF4.dev over HTTP.
This guide covers both Vercel runtimes precisely, the bundle size and cold start tradeoffs, the maxDuration setting, and a working Next.js route handler. Vercel runs on AWS Lambda under the hood, so the same Chromium constraints that apply to Lambda apply here.
Which Vercel approach should you use for PDFs?
The choice comes down to which runtime your route uses and whether you want to bundle a browser at all. The table below compares the three realistic options on Vercel.
| Approach | Runtime | Bundle impact | Cold start | Edge support | Best for |
|---|---|---|---|---|---|
playwright-core + @sparticuz/chromium | Node.js | ~45 MB compressed | 2 to 5s first call | No | Full HTML/CSS fidelity, self-hosted |
puppeteer-core + @sparticuz/chromium | Node.js | ~45 MB compressed | 2 to 5s first call | No | Same fidelity, smaller API surface |
| Hosted render API (PDF4.dev) | Node.js or Edge | Near zero | None on your side | Yes | No browser to bundle, runs anywhere |
Bundle and cold start figures are approximate and vary by Chromium version, function memory, and document complexity.
The split is simple. If your code must run Chromium, you are locked to the Node.js runtime and you pay the bundle and cold start cost. If you call out to an HTTP service, the runtime no longer matters and Edge becomes viable.
Why can the Edge runtime not generate PDFs?
The Vercel Edge runtime cannot generate PDFs with a browser because it exposes only Web APIs inside a V8 isolate, with no filesystem, no child processes, and no native binary execution. Headless Chromium is a native executable that the renderer must spawn as a child process, so there is no code path for it to start on Edge.
This is a hard architectural limit, not a configuration you can flip. The Edge runtime is built on the same primitives as Cloudflare Workers and Deno Deploy: fast cold starts and global distribution in exchange for no Node.js APIs like fs, child_process, or native addons. @sparticuz/chromium reads its binary from the filesystem and launches a process, so importing it on Edge throws at module load.
Setting export const runtime = "edge" on a route that imports @sparticuz/chromium or playwright-core will fail the build or crash at runtime. These packages depend on Node.js APIs that the Edge runtime does not provide. Keep browser-based rendering on the Node.js runtime.
The only way to return a PDF from an Edge function is to ask another machine to render it. That machine can be your own Node.js function elsewhere, a container, or a hosted API. From Edge, the render call is a plain fetch, which is exactly what the Web API surface supports.
How do you render a PDF in a Vercel Node.js function?
Use the Node.js runtime, install playwright-core and @sparticuz/chromium, launch the browser from the slim binary, and call page.pdf(). The key is playwright-core (no bundled browsers) plus @sparticuz/chromium for an executable that fits the function size limit. A normal playwright install downloads a full Chromium that blows past the bundle cap.
Install the two packages:
npm install playwright-core @sparticuz/chromiumThen write a Next.js App Router route handler. The two config exports matter: runtime = "nodejs" keeps you off Edge, and maxDuration gives Chromium time to cold start and render.
// app/api/pdf/route.ts
import { NextResponse } from "next/server"
import chromium from "@sparticuz/chromium"
import { chromium as playwright } from "playwright-core"
// Pin to the Node.js runtime. Edge cannot run Chromium.
export const runtime = "nodejs"
// Cold start plus render can exceed the 10s Hobby default.
export const maxDuration = 60
export async function POST(req: Request) {
const { html } = await req.json()
const browser = await playwright.launch({
args: chromium.args,
executablePath: await chromium.executablePath(),
headless: true,
})
try {
const page = await browser.newPage()
await page.setContent(html, { waitUntil: "networkidle" })
const pdf = await page.pdf({
format: "A4",
printBackground: true,
margin: { top: "20mm", bottom: "20mm", left: "15mm", right: "15mm" },
})
return new NextResponse(new Uint8Array(pdf), {
headers: {
"Content-Type": "application/pdf",
"Content-Disposition": 'attachment; filename="document.pdf"',
},
})
} finally {
await browser.close()
}
}Two things break people most often. First, await browser.close() in a finally block: a leaked browser keeps the Lambda warm and burns memory across invocations. Second, the runtime and maxDuration exports: omit them and you inherit the 10 second Hobby default, which Chromium routinely exceeds on a cold start.
What are the real limits of Chromium on Vercel?
Three limits decide whether browser-based PDFs on Vercel hold up: bundle size, cold start latency, and execution time. Each has a number worth knowing before you ship.
Bundle size. A Vercel serverless function is capped near 250 MB uncompressed and roughly 50 MB compressed on most plans. A full Playwright Chromium is around 150 MB unpacked and will not fit. @sparticuz/chromium exists specifically to squeeze under that ceiling, landing near 45 MB compressed. Check the package's README for the version matrix, since the Chromium build must match the playwright-core or puppeteer-core version you install.
Cold start. The first request to a cold function has to unpack and launch Chromium, which commonly adds 2 to 5 seconds before any rendering starts. Warm invocations skip the unpack and are far faster. Raising the function memory speeds the unpack, since Lambda scales CPU with memory.
Execution time. maxDuration defaults to 10 seconds on Hobby. Pro and Enterprise allow up to 300 seconds. Cold start plus a multi-page render can blow past 10 seconds, so set maxDuration = 60 or higher for anything beyond a one-page document. See the Vercel functions configuration docs for current per-plan ceilings.
Set function memory to 1024 MB or more for Chromium rendering. Lambda allocates CPU proportionally to memory, so more memory means faster unpack and faster rendering, which often pays for itself by finishing inside maxDuration.
How does the hosted PDF4.dev approach work on Vercel?
PDF4.dev renders the PDF on its own infrastructure, so your Vercel function only makes one HTTP request. No Chromium in your bundle, no cold start on your side, and it runs from any runtime including Edge, because a render is just a fetch to https://pdf4.dev/api/v1/render. You send HTML (or a template id) and data, you get back a PDF.
The minimal request from a Vercel function, on any runtime:
curl -X POST https://pdf4.dev/api/v1/render \
-H "Authorization: Bearer p4_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"html": "<h1>Hello from Vercel</h1>",
"data": {},
"delivery": "url"
}'The delivery: "url" option returns a signed link to the rendered PDF instead of a base64 blob, which keeps the response small. That matters on Edge, where holding a large binary in memory is wasteful, and it matters for any function that would otherwise return a multi-megabyte payload.
PDF4.dev uses headless Chromium with Handlebars {{variables}} server-side, so HTML and CSS fidelity matches what you would get from running Playwright yourself, without you owning the browser pool, the cold starts, or the bundle size math. It is the no-infrastructure option in the table, one valid choice alongside self-hosting.
Which option should you choose?
Pick by where your render needs to run and how much infrastructure you want to own. The recommendation splits cleanly by scenario.
| Scenario | Recommended option |
|---|---|
| Must run on the Edge runtime | Hosted API (PDF4.dev) over fetch |
| Already on Node.js, low volume, full control | playwright-core + @sparticuz/chromium |
| High volume, want no cold starts or bundle limits | Hosted API (PDF4.dev) |
| Existing Puppeteer codebase | puppeteer-core + @sparticuz/chromium |
| Need pixel-perfect CSS and your own infra | Self-host Chromium on Node.js runtime |
Choose @sparticuz/chromium if you are comfortable on the Node.js runtime, your volume is low enough that cold starts are tolerable, and you want the rendering to stay inside your own deployment. You own the Chromium version, the bundle, and the maxDuration tuning.
Choose the hosted PDF4.dev API if you need Edge support, you do not want to bundle a 45 MB browser, or you want consistent render latency without warming functions. The tradeoff is an external dependency and an HTTP round trip instead of an in-process call.
For prototyping a layout before you wire any of this up, the free Html To PdfTry it free and Webpage To PdfTry it free tools render HTML to PDF in the browser so you can check fidelity first. Then move to whichever runtime path fits your traffic.
Key takeaways
- The Vercel Node.js runtime can run Chromium via
playwright-coreorpuppeteer-coreplus@sparticuz/chromium. The Edge runtime cannot run a browser at all. - Bundle size (~45 MB compressed), cold starts (2 to 5s), and
maxDuration(raise above the 10s default) are the three constraints to plan around. - Always pin
export const runtime = "nodejs"and close the browser in afinallyblock. - A hosted API like PDF4.dev removes the browser entirely and works from any runtime, including Edge, since rendering becomes a single
fetchcall.
Free tools mentioned:
Start generating PDFs
Build PDF templates with a visual editor. Render them via API from any language in ~300ms.



