Cloudflare Workers
PDF generation on Cloudflare Workers
You installed Puppeteer, deployed, and it died. That is expected: Workers run on workerd, which has no native binaries, no process spawning and no filesystem. Here are the three approaches that do work, and the honest trade-offs of each.
Why the obvious approach fails
Every headless-browser library assumes a Node.js host. workerd deliberately provides none of what they need:
No native binaries
Chromium is a compiled executable, not JavaScript.
No process spawning
child_process does not exist, so nothing can launch a browser.
No filesystem
Puppeteer's download step and its user-data directory have nowhere to live.
A bounded heap
Even a stripped Chromium would not fit in a Worker's memory budget.
This is a runtime boundary, not a configuration mistake. No combination of nodejs_compat, bundler flags or a smaller Chromium build gets you across it.
The four options, compared
| Approach | Works on Workers? | Cost | Real ceiling | Best for |
|---|---|---|---|---|
| npm i puppeteer / playwright | No — never runs | — | workerd has no native binaries, no child processes, no filesystem | Nothing. This is the wall most people hit first. |
| Cloudflare Browser Rendering | Yes — @cloudflare/puppeteer via a browser binding | Free plan: 10 browser-minutes/day. Paid: usage-based | Free: 3 concurrent browsers, 1 new browser / 20s. Paid: 200 concurrent, 3 / second | Capturing live URLs, authenticated pages, running JS, crawling |
| External browser over WebSocket | Yes — connect to a hosted Chrome | Per-session or per-hour, billed by the provider you pick | You manage sessions, timeouts and pool exhaustion | Complex automation you already run elsewhere |
| A render API (Renderfy) | Yes — a plain fetch, no binding | From $9/mo; 3 credits per PDF, 1 per image | 1 MB payload per render, 20 renders per batch request | Turning markup you already have into a PDF or image |
Browser Rendering limits are Cloudflare's published figures for the Workers Free and Paid plans. Check their docs before relying on them — they change.
Which one you actually want
Use Browser Rendering if the input is a URL
Screenshotting a live page, logging into a dashboard, waiting on client-side JavaScript, crawling — these need a real browser session, and Cloudflare's is well-integrated and priced fairly. Budget for session-reuse code and the concurrency caps above.
Use a render API if the input is markup
Invoices, receipts, reports, certificates, share cards, charts and diagrams are documents your Worker already has the data for. Launching a browser to print a string you generated three lines earlier is a lot of moving parts for a formatting step — and it consumes browser-minutes that a fetch does not.
A working Worker
No binding, no nodejs_compat, no browser lifecycle. Store the key with npx wrangler secret put RENDERFY_API_KEY.
// src/index.ts
interface Env {
RENDERFY_API_KEY: string;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const { searchParams } = new URL(request.url);
const customer = searchParams.get("customer") ?? "Acme Inc.";
const res = await fetch("https://renderfy.io/api/v1/render", {
method: "POST",
headers: {
Authorization: `Bearer ${env.RENDERFY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
type: "html",
content: `<html><body style="font-family: system-ui; padding: 48px">
<h1 style="margin:0 0 8px">Invoice</h1>
<p style="color:#666; margin:0">Billed to ${customer}</p>
</body></html>`,
options: { format: "pdf", dimensions: "a4" },
}),
});
if (!res.ok) {
return new Response("Render failed", { status: 502 });
}
return new Response(await res.arrayBuffer(), {
headers: {
"Content-Type": "application/pdf",
"Content-Disposition": 'attachment; filename="invoice.pdf"',
},
});
},
};Change type to tailwind, markdown, code, chart, diagram or math depending on the input you have, and format to "png", "jpeg" or "webp" for an image instead.
The part that matters at scale
A Worker is billed on CPU time, and awaiting a network call is not CPU time. With a render API the heavy work — layout, font resolution, rasterisation, PDF assembly — happens on someone else's machine while your Worker sits idle. With Browser Rendering it happens against your account's browser-minutes and concurrency budget. For a low-frequency document endpoint that difference is invisible; for a per-request PDF on a busy route it is the whole cost model.
Frequently asked questions
Why does Puppeteer not work on Cloudflare Workers?+
Workers run on workerd, not Node.js. A stock npm install puppeteer expects to download and spawn a Chromium binary, which requires native binaries, process spawning, shared libraries, a filesystem and a large heap — none of which exist in the Workers runtime. This is not a bundle-size problem you can tune around; Chromium is roughly 300 MB and could not be shipped in a Worker even if the runtime allowed it.
Can I use Cloudflare Browser Rendering instead?+
Yes, and for some jobs it is the right answer. Cloudflare maintains @cloudflare/puppeteer, which talks to managed Chromium instances through a browser binding you declare in wrangler.toml. If you need to load a live URL, execute its JavaScript, log in, or crawl, use Browser Rendering — a render API cannot do those things.
What are the Browser Rendering limits?+
On the Workers Free plan: 3 concurrent browsers per account, one new browser every 20 seconds, a 60-second idle timeout, and 10 browser-minutes per day. On the Workers Paid plan: 200 concurrent browsers, 3 new browsers per second, the same 60-second idle timeout extendable to 10 minutes with keep_alive, and no daily cap. Exceeding a limit returns HTTP 429. Reusing sessions is essential, which means you own browser lifecycle code.
When is a render API the better choice on Workers?+
When you are not automating a browser, you are producing a document. If the input is markup, Markdown, a chart definition or a diagram that your Worker already has in memory, spinning up a browser session to print it is a large amount of machinery for a formatting job. A render API is one fetch, has no binding to configure, no session pool to size, no concurrency cap tied to your account, and works on the Workers Free plan.
Does Renderfy need a Worker binding or nodejs_compat?+
No. Rendering is a single HTTPS POST, so a plain fetch works on workerd unmodified — no browser binding, no nodejs_compat flag, no wrangler configuration beyond a secret for your API key. @renderfy/sdk is a zero-dependency TypeScript client that also lists Cloudflare Workers as a supported runtime if you prefer a typed wrapper.
How long does a render take against the Worker CPU limit?+
Almost none of it counts. Awaiting a fetch is I/O, not CPU, so the render happens on our servers while your Worker is idle. This is the practical difference from Browser Rendering, where launching and driving a browser consumes your own Worker's wall-clock budget and browser-minutes.
Related
Ship the PDF endpoint today
100 free credits, no card required. One fetch from your Worker — no binding, no browser to babysit.