Skip to content

Generate PDFs in Cloudflare Workers

Import renderPdf from fullbleed/cloudflare to turn static HTML/CSS into PDF bytes inside a Worker. Wrangler bundles the engine and fonts with your code. The renderer uses an in-memory filesystem; document content does not need to leave the Worker for a PDF service.

The adapter is verified in local workerd on Windows and Linux. A hosted Playground smoke test also reproduces the starter's PDFs. Production CPU, memory, startup behavior and Free-plan suitability remain unqualified. Measure representative documents in your deployment before choosing a plan or workload limits. Cloudflare's platform limits apply to the entire isolate, including WebAssembly memory.

Run a complete invoice endpoint

Download the Workers invoice starter Open the generated PDF See a five-page invoice

Northstar Studio invoice with forest-green typography, project progress bars and a dark-green total panel.

The project uses npm package 0.8.0, engine 2.5.29, and Wrangler 4.151.3. It accepts structured invoice JSON, applies an editable template, and returns a PDF attachment. The sample organization and customer are fictional.

Extract the ZIP and, with Node.js 22 or newer, run:

npm ci --ignore-scripts
npm test
npm run build
npm run dev

build produces a local bundle without deploying. dev starts the local Worker at http://127.0.0.1:8787; no Cloudflare account is needed. In a second terminal:

curl --fail-with-body http://127.0.0.1:8787/invoice \
  -H "Content-Type: application/json" \
  --data-binary @sample.json --output invoice.pdf

Open invoice.pdf. On Windows PowerShell, use curl.exe and put that command on one line. Run npm run test:worker while the server is running to check sample, zero-total, maximum-total, long, escaped-text and invalid invoices. The test uses synthetic data and saves its PDFs in output/.

Use your own HTML and CSS

The adapter accepts the same static document sources as the Node renderer:

import { renderPdf } from 'fullbleed/cloudflare';

export default {
  async fetch() {
    const result = await renderPdf({
      html: '<h1>Invoice NS-1042</h1><p>Design and delivery: $1,870.00</p>',
      css: '@page { size: A4; margin: 20mm } h1 { color: #173b35 }',
      maxPages: 5,
    });
    return new Response(result.pdf, {
      headers: {
        'Content-Type': 'application/pdf',
        'Content-Disposition': 'attachment; filename="invoice.pdf"',
        'Cache-Control': 'no-store',
      },
    });
  },
};

This is a complete Worker entry file. Keep document input behind your application's authorization and workload limits. HTML scripts do not run, and document URLs do not fetch remote resources. Supply images and custom fonts as byte arrays through assets and fonts.

Add this rule to an existing Wrangler configuration so the bundled TrueType fonts become data modules:

{
  "rules": [{ "type": "Data", "globs": ["**/*.ttf"], "fallthrough": false }]
}

Wrangler already recognizes the engine's compiled .wasm import. The download includes the complete configuration; see Cloudflare's WebAssembly module documentation for the runtime mechanism.

Edit the starter's design

File What to change
sample.json Customer, reference, dates, paid status, and line items.
src/document.mjs Headings, labels, document sections, and table rows.
src/styles.mjs Page geometry, fonts, colors, spacing, and class rules.
src/handler.mjs Input schema, limits, and download headers.
src/index.mjs The platform renderer import.

This template uses JavaScript function components. className adds an HTML class, and a matching rule in the document's StyleSheet supplies its styling. Invoice({ data }).renderPdf(renderPdf) passes the generated HTML and CSS to the Workers renderer. You can replace that call with your own HTML/CSS template.

For standalone HTML, use Invoice({ data }).toHtml({ includeStyles: true }). The embedded stylesheet also renders to PDF with css: '' and defaultStyles: false. includeStyles: false exports HTML without styles.

The sample accepts 1–30 rows and at most 32 KiB of JSON. Rates are nonnegative integer cents, quantities are integers, dates use YYYY-MM-DD, and the total is limited to $999,999.99. It uses USD and does not calculate tax. Tables flow across pages, with a ten-page render limit. Change these bounds together with layout tests for your own documents.

Connect it to your application

The starter disables workers_dev and preview_urls and defines no public route. Its local endpoint has no authentication. Configure an authenticated application route or private service binding before deployment, apply rate and concurrency limits, and keep invoice data out of logs. The handler stores no invoices or PDFs and returns Cache-Control: no-store.

Rendering is synchronous inside the isolate and returns a settled Promise. Each call gets a fresh engine instance, but that does not guarantee that previous allocations have already been garbage-collected. timeoutMs, signal, and isolation are rejected on this entry. Cloudflare enforces CPU and memory limits; page and input limits are not guarantees about either resource.

The adapter defaults to 20 pages, caps HTML/CSS at 1,000,000 UTF-8 bytes, and caps caller-provided fonts and assets at 8 MiB combined. Optional previews use additional memory. These are ordinary PDFs; this entry does not expose PDF/A, PDF/UA, PDF/X, or VDP profiles.

Verification and source

The release checks the installed npm tarball in real local workerd, compares PDF and preview bytes with Node, and exercises custom fonts, SVG assets, components, embedded styles, input limits, missing glyphs and failure recovery. The downloadable project is also installed from its lockfile and exercised over local HTTP. Independent readers check the retained PDF contents, and the sample and long-invoice layouts are visually reviewed.

Hosted Playground smoke test

The released 0.8.0 starter was also exercised in Cloudflare's account-free Workers Playground on October 21, 2026 (UTC). Six invoices, including a five-page invoice and recovery after invalid requests, matched the local PDF bytes exactly. An independent reader checked the ten output pages and expected text. Ten invalid inputs, method restrictions and unknown routes returned the expected responses. The temporary preview was replaced with a 410 response after the checks.

This establishes hosted sandbox behavior for those fixtures. CPU and memory were not measured, and this was not a load test or a production account deployment. The Playground's service source at the checked revision selects an unbound usage model, so this result does not establish Free-plan suitability. The hosted verification record retains the module and PDF hashes and source references.

Download source manifest · Local verification record · SDK guide · Release evidence