Serverless PDF generation without Chromium: which approach actually deploys

Two meanings of "without Chromium", the non-browser engines that fit serverless and edge runtimes, and when a hosted API is the better answer.
You can generate PDFs on serverless platforms without Chromium in two distinct ways, and confusing them is why most people stay stuck. Either swap the headless browser for a non-browser engine that draws the document directly — pdf-lib, react-pdf, WeasyPrint, or Satori + resvg for edge runtimes — which removes the browser entirely but trades away some CSS and JavaScript support. Or keep browser-quality rendering and move it off your function by calling a hosted PDF API, which keeps full fidelity and removes only the infrastructure you operate. Which is right depends on whether your document needs a real browser or just needs to stop breaking your deploy.
Disclosure: Paperbase is a hosted PDF API and one of the options compared below. The trade-offs here are the ones I'd give a friend, including the cases where you should skip a hosted API entirely.
Decision summary
- Use a non-browser engine when your layout is simple or fixed, you can express it in the engine's supported subset, and you want the smallest possible footprint — especially at the edge.
- Use a hosted PDF API when the document is a branded report, proposal, or invoice that needs real browser fidelity and reliable pagination, and you don't want to operate a browser to get it.
- Main advantage of dropping the browser: a tiny, fast, edge-friendly deploy with no cold-start tax.
- Main trade-off: each non-browser engine drops a different slice of CSS, JavaScript, or chart support — the cost is fidelity, and it's invisible until a real document flows through.
- Default recommendation: decide by the document, not the tool. Fixed cards and receipts suit a non-browser engine; branded and AI-generated documents usually want browser-class rendering you don't have to run.
Why does Chromium fail on serverless in the first place?
Because a serverless function is not a server. It's a micro-VM with a read-only filesystem (except /tmp) and a stripped-down Linux image missing the libraries a browser expects, as one production post-mortem lays out in detail. When your code calls puppeteer.launch(), it looks for a Chromium binary that either isn't there or can't run.
The size math makes it worse. The standard puppeteer package bundles Chrome for Testing at roughly 170–280 MB, against Lambda's 50 MB zipped / 250 MB unzipped limits and Vercel's comparable function cap. The well-trodden workaround — puppeteer-core plus @sparticuz/chromium (~50 MB compressed) — fits, but you now own the bundle dance, and the minimized build often has to be hosted on S3 or attached as a Lambda layer to stay under the limit. Vercel itself maintains a knowledge-base walkthrough for the workaround — a fair signal that the unhappy path is the default path.
Even once it deploys, the failure strings are familiar: Could not find Chrome and libnss3.so: cannot open shared object file are recurring, documented on Vercel's own community forum. And a cold start of one to three seconds on Lambda can exceed a Hobby-tier function's execution timeout before the first render begins.
The honest reframe: serverless was never built to host a browser, so the real question isn't how to squeeze Chromium in — it's whether you need a browser at all.
What does "without Chromium" actually mean?
Two very different things:
- No browser anywhere. Replace the browser-based renderer with a programmatic or purpose-built engine. There is no Chromium to ship, patch, or keep warm — but you accept whatever slice of CSS and JavaScript that engine supports.
- No browser in your function. Keep browser-class fidelity; stop operating Chromium yourself by calling a hosted API. The browser still runs — it just isn't your cold start, your bundle, or your 2 a.m. page.
Most people typing this query believe they want option 1. Many actually want option 2: they don't dislike Chromium's output, they dislike operating it. Naming which one you're after saves days.
How do the non-browser engines compare?
These draw the document directly instead of rendering a browser, so there's no Chromium dependency at all.
- pdf-lib / PDFKit / pdfmake construct PDFs from primitives — text runs, vectors, existing pages. Tiny, fully deterministic, edge-friendly. The catch: no HTML or CSS, so you build layout by hand.
- react-pdf (
@react-pdf/renderer) lets you write layouts in JSX with a constrained component set and a CSS subset. It's popular for a reason — ~860k weekly npm downloads as of mid-2025 — but Flexbox works while CSS Grid and pseudo-selectors don't, existing HTML templates need a rewrite, and charts/SVG (recharts) are a known weak spot. - WeasyPrint is a purpose-built CSS engine with no Chromium dependency and Docker images around 200–400 MB. It shines at CSS Paged Media — running headers, footnotes, margin boxes. The costs: it executes no JavaScript at all, spawns a fresh process per render, and gets slow on big documents (a 52-page file reportedly took ~100 seconds; 5,000-row tables consumed 1.4+ GB RAM).
- Typst compiles its own markup language to PDF — fast and small, and built partly because LaTeX distributions exceed 1 GB and don't suit serverless. The trade-off is that it isn't HTML; it's a new DSL to learn.
- Satori + resvg is the one path that genuinely runs at the edge. It renders HTML/JSX to SVG, then rasterizes — a ~10 MB dependency at ~50–200 ms that works in Cloudflare Workers, Vercel Edge, and Lambda@Edge. It's the engine behind
@vercel/og. The limits are real: Flexbox only (no Grid, noz-index), no JavaScript execution, and it's PNG-first, so PDF needs a wrapper step.
A short avoidance note: wkhtmltopdf still appears in tutorials, but it was archived in January 2023 on a frozen ~2012 engine with unpatched security issues. Treat it as debt, not a choice.
Here is the honest rubric, extended to the hosted option:
| Criterion | Puppeteer / Playwright | pdf-lib / PDFKit | react-pdf | WeasyPrint | Satori + resvg | Hosted PDF API (Paperbase) | |---|---|---|---|---|---|---| | Rendering model | Headless browser | Programmatic drawing | React → PDF primitives | Purpose-built CSS engine | HTML/JSX → SVG → raster | Hosted browser-class | | HTML & CSS fidelity | Full (it's Chrome) | None (build by hand) | CSS subset, no Grid | CSS Paged Media, no JS | Flexbox subset, no Grid | Full HTML/CSS + Markdown in | | Runs at the edge | No | Yes | Node/serverless | No (process per render) | Yes | Called from anywhere | | Infrastructure | You operate Chromium | None | None | Heavy Python runtime | None | None (managed) | | JavaScript execution | Yes | N/A | N/A | No | No | Yes | | Performance | Cold starts, ~100+ MB | Fast, tiny | Sub-second, ~2 MB | Slow on big docs | ~50–200 ms | Managed; no cold start in your app | | Best fit | Own server + automation | Precise, minimal docs | React apps, simple layouts | CSS-only print docs | Fixed edge cards | Serverless / AI-built branded docs | | Poor fit | Serverless, high concurrency | Rich HTML layouts | Grid / chart-heavy | Anything needing JS | Grid / complex layouts | Full browser automation |
Can you generate PDFs at the edge?
Yes — but the field narrows fast. Puppeteer and Playwright can't run in a V8-isolate edge runtime at all; there's no Chromium there to launch. WeasyPrint needs a Python process and a 200 MB+ image, so it's out too. That leaves the programmatic libraries (pdf-lib, PDFKit) and Satori + resvg, which is the only HTML-shaped path that truly runs inside an edge sandbox.
Worth a footnote on platform churn: Vercel's 2025 Fluid Compute reduced cold-start pain, and Vercel deprecated Edge Functions while the Edge runtime persists inside Serverless Functions — two things easily conflated. And Cloudflare offers a managed Browser Rendering binding for Workers if you specifically need real Chromium at the edge without shipping it yourself.
A compact Satori-to-PDF sketch (edge-compatible):
// Edge: HTML/JSX → SVG (Satori) → PNG (resvg) → embed in a PDF (pdf-lib)
import satori from "satori";
import { Resvg } from "@resvg/resvg-js";
import { PDFDocument } from "pdf-lib";
const svg = await satori(<Report data={data} />, { width: 794, height: 1123, fonts });
const png = new Resvg(svg).render().asPng();
const pdf = await PDFDocument.create();
const page = pdf.addPage([794, 1123]);
page.drawImage(await pdf.embedPng(png), { x: 0, y: 0, width: 794, height: 1123 });
const bytes = await pdf.save(); // no browser involved
It works — for fixed, Flexbox-only layouts with no dynamic charts. Push past that subset and you're back to needing a browser.
What do you give up by dropping the browser?
The thing you were paying Chromium for: fidelity. Concretely, moving to a non-browser engine can cost you CSS Grid, z-index, pseudo-selectors, JavaScript-drawn charts (Chart.js, recharts), and the layout engine's automatic pagination — repeating table headers on continuation pages, sane page breaks, widow/orphan control. Chromium's own print path already gets table headers wrong by default; the lighter engines simply hand more of that back to you.
There's a raw-speed picture too. A 2026 benchmark clocked warm Playwright at ~3 ms for a simple document versus WeasyPrint's ~629 ms — while WeasyPrint produced far smaller files (21 KB vs 125 KB) but can't keep a warm instance, since it spawns a process per render. In other words, "lighter" and "faster" aren't the same axis, and neither is "no browser" the same as "no cost."
When is a hosted PDF API the better "without Chromium"?
When the honest answer to "do I need browser fidelity?" is yes, but the answer to "do I want to run a browser?" is no. That's the whole of option 2. A hosted API renders with a real browser on someone else's infrastructure, so your function stays a thin HTTP call — no binary in the bundle, no cold start in your app.
For Paperbase specifically, you send Markdown or HTML and get back a paginated, brand-themed PDF, with pagination fidelity (repeating headers, sane breaks, font determinism) handled by default. Errors come back as structured warning codes a coding agent can act on.
// The whole integration — no Chromium to deploy
import { Paperbase } from "paperbase";
const pb = new Paperbase({ apiKey: process.env.PAPERBASE_API_KEY! });
const { url } = await pb.pdf.generate({
input: { type: "markdown", content: report },
template: "report",
theme: { logo_url: "https://…", accent_color: "#2563eb" },
});
Self-hosting sits in between: Gotenberg is an MIT, container-native renderer you run off your serverless function — still Chromium, just not in your Next.js route. It's the "I'll operate it, just not there" path.
Can I use both?
Yes, and many teams should. A common split: keep a programmatic library (pdf-lib, react-pdf) for the trivial deterministic documents, route branded reports, proposals, and agent-generated output to a hosted API, and reach for Puppeteer only where you genuinely need browser automation. The decision is per-workload, not per-company.
Which approach fits which use case?
- Structured receipt or simple invoice, fixed fields: pdf-lib or PDFKit. Deterministic and tiny.
- React app, simple non-Grid branded layout: react-pdf — as long as you don't need charts.
- CSS-only print document with running headers/footnotes, no JavaScript: WeasyPrint.
- A fixed card or certificate you must render at the edge: Satori + resvg.
- A branded report, proposal, or AI-generated document on serverless — where fidelity and pagination matter and no one's babysitting a browser: a hosted PDF API. This is the case Paperbase is built for.
Tested-on note (2026-06-30): the serverless Chromium failure modes above were reproduced on Next.js (App Router) deployed to Vercel with
puppeteer-core+@sparticuz/chromium, Node 20.
FAQ
Can I run Puppeteer on Cloudflare Workers or Vercel Edge? No — edge runtimes are V8 isolates with no Chromium to launch. Use Satori + resvg for fixed layouts, Cloudflare's managed Browser Rendering binding, or a hosted API called from the edge.
Does react-pdf need Chromium? No. It's programmatic (React → PDF primitives), so it deploys cleanly to serverless. The trade-off is a CSS subset — Flexbox but no Grid — and weak chart/SVG support.
What's the lightest way to make a PDF on the edge? Satori + resvg (~10 MB, ~50–200 ms) for Flexbox-only fixed layouts, or pdf-lib for programmatic documents. Anything needing CSS Grid or JavaScript charts won't fit that subset.
Why does my PDF work locally but fail on Vercel or Lambda?
Your machine has a full OS and a real Chromium; the serverless function has neither. The binary is missing or too large, and system libraries like libnss3 aren't present.
Is WeasyPrint a good fit for serverless? It removes the browser and handles CSS Paged Media well, but it runs no JavaScript, needs a 200 MB+ Python runtime, and spawns a process per render — so it's better on a long-running server than in a lean function.
Do hosted PDF APIs still use Chromium? Usually yes — but on their infrastructure, not in your function. You keep browser fidelity without shipping, patching, or cold-starting a browser yourself.
Recommendation
If your document is simple or fixed and fits a supported subset, drop the browser: pdf-lib for programmatic output, react-pdf for React layouts, WeasyPrint for CSS-only print, Satori + resvg for the edge. If it's a branded or AI-generated document that needs real browser fidelity and reliable pagination, keep the fidelity and move the browser off your function with a hosted API. Decide by the document, not the tool — that single reframe resolves most of the "without Chromium" confusion.
Need browser-quality output without operating Chromium anywhere in your stack? Render the same document with Paperbase and compare the deploy side by side.