Generating PDFs server-side typically means wrestling with Lambda cold starts, managing Chromium binaries, or dealing with third-party APIs that add latency and cost. If you’ve ever spun up a 1GB+ Docker container just to convert HTML to PDF, you know the pain.
Cloudflare Browser Rendering flips this model. It’s a managed headless Chromium service that runs on Cloudflare’s edge network, i.e. no binaries to bundle, no cold starts, no Docker layers. You get Puppeteer-compatible APIs that execute in a globally distributed with sub-100ms startup times.
In this guide, you’ll build a production-ready PDF generation system using Astro, Cloudflare KV and Cloudflare Workers. You’ll create printable receipt pages with custom CSS, wire up an API endpoint that launches headless Chrome at the edge, and cache the results in Cloudflare KV to avoid redundant rendering.
Prerequisites
You’ll need:
- Node.js 20 or later
- A Cloudflare account
Why Cloudflare Browser Rendering for PDFs?
- No Chromium babysitting: Cloudflare runs and patches headless Chromium for you.
- Edge locality: Render PDFs close to the requester; great for dashboards, invoices, and OG images.
- Streaming: Send PDFs directly from the worker without temp files.
- Cost-aware: Pay for usage without maintaining a fleet of renderers.
Architecture overview
1. Astro Page (src/pages/receipt/[id].astro) → SSR route that renders your receipt/invoice HTML + print CSS
2. PDF API (src/pages/api/pdf/[id].ts) → Creates a Browser Rendering session → Loads the Astro route URL → Calls page.pdf() and streams the binary
3. Cloudflare Adapter + Browser binding → Deploys Astro to Workers/Pages → Provides a BROWSER binding to the workerCreate a new Astro application
Let’s get started by creating a new Astro project. Execute the following command:
npm create astro@latest my-astro-kv-pdf-appcd my-astro-kv-pdf-appnpm install wrangler @cloudflare/puppeteerWhen prompted, choose:
Use minimal (empty) templatewhen prompted on how to start the new project.Yeswhen prompted to install dependencies.Yeswhen prompted to initialize a git repository.
Once that’s done, you can move into the project directory and start the app:
npm run devThe app should be running on localhost:4321.
Integrate Cloudflare adapter in your Astro project
To deploy your Astro project to Cloudflare Workers and use Cloudflare KV, you need to install the Cloudflare adapter. Execute the command below:
npx astro add cloudflare --yesUpdate astro.config.mjs to have output as server:
import { defineConfig } from 'astro/config';import cloudflare from '@astrojs/cloudflare';
export default defineConfig({ output: 'server', adapter: cloudflare()});Configure Browser Rendering in wrangler.jsonc
Add a BROWSER binding so the worker can launch headless Chromium:
{ "main": "dist/_worker.js/index.js", "name": "my-astro-kv-pdf-app", "compatibility_date": "2025-12-06", "compatibility_flags": [ "nodejs_compat", "global_fetch_strictly_public" ], "assets": { "binding": "ASSETS", "directory": "./dist" }, "observability": { "enabled": true }, "browser": { "binding": "BROWSER" }}The
browserblock provisions Cloudflare’s Browser Rendering service and exposes it to your worker asenv.BROWSER.
Add KV for caching
Create a KV namespace to cache generated PDFs:
npx wrangler kv namespace create PDF_CACHEThis will output an id that you’ll add to wrangler.jsonc. Update the file to include the KV binding:
{ "main": "dist/_worker.js/index.js", "name": "my-astro-kv-pdf-app", "compatibility_date": "2025-12-06", "compatibility_flags": [ "nodejs_compat", "global_fetch_strictly_public" ], "assets": { "binding": "ASSETS", "directory": "./dist" }, "observability": { "enabled": true }, "browser": { "binding": "BROWSER" }, "kv_namespaces": [ { "binding": "PDF_CACHE", "id": "generated-id", "remote": true } ]}Add TypeScript types
Create src/env.d.ts to add proper types for the Cloudflare bindings:
/// <reference types="astro/client" />
type KVNamespace = import('@cloudflare/workers-types').KVNamespace
type ENV = { PDF_CACHE: KVNamespace BROWSER: Fetcher}
type Runtime = import('@astrojs/cloudflare').Runtime<ENV>
declare namespace App { interface Locals extends Runtime {}}This gives you full type safety when accessing locals.runtime.env.PDF_CACHE and locals.runtime.env.BROWSER in your routes.
Build a printable Astro page
Create a receipt page with print-friendly styles:
---const { id } = Astro.params;const order = { id, customer: 'Ada Lovelace', total: '$128.00', items: [ { name: 'Edge Functions', price: '$50.00' }, { name: 'PDF Rendering', price: '$78.00' } ]};---
<html lang="en"> <head> <meta charset="UTF-8" /> <title>Receipt #{order.id}</title> <style> :root { font-family: 'Inter', system-ui, -apple-system, sans-serif; } body { margin: 0; padding: 32px; color: #0f172a; } header { display: flex; align-items: center; justify-content: space-between; } table { width: 100%; border-collapse: collapse; margin-top: 24px; } th, td { padding: 12px 8px; text-align: left; border-bottom: 1px solid #e2e8f0; } .total { text-align: right; font-size: 1.125rem; font-weight: 700; } @page { margin: 24mm; } @media print { body { background: white; } a[href]::after { content: ''; } } </style> </head> <body> <header> <div> <h1>Receipt #{order.id}</h1> <p>Issued to {order.customer}</p> </div> <strong>launchfa.st</strong> </header> <table> <thead> <tr><th>Item</th><th>Price</th></tr> </thead> <tbody> {order.items.map(item => ( <tr> <td>{item.name}</td> <td>{item.price}</td> </tr> ))} </tbody> </table> <p class="total">Total: {order.total}</p> </body></html>Save it as src/pages/receipt/[id].astro. The print CSS (@page) sets margins, and the @media print rules remove noisy link decorations.
Create the PDF API route
Here’s how to create a Worker-style API route that uses Cloudflare’s browser rendering to generate and cache a PDF version of your Astro HTML receipt page.
We’ll set up an API endpoint (src/pages/api/pdf/[id].ts) that:
- checks if a PDF is already cached in Cloudflare KV and returns it immediately if available
- launches a headless browser via the Cloudflare Browser Rendering API if not cached
- navigates to the printable page and generates a PDF
- stores the resulting PDF in KV for 7 days for future requests
import type { APIRoute } from "astro";import puppeteer from "@cloudflare/puppeteer";
// Helper to create a proper PDF HTTP responseconst pdfResponse = (pdf: Buffer<ArrayBuffer>, id: string) => { return new Response(pdf, { headers: { 'Content-Type': 'application/pdf', 'CDN-Cache-Control': 'max-age=604800', 'Content-Disposition': `inline; filename="receipt-${id}.pdf"` } });}
export const GET: APIRoute = async ({ locals, params, request }) => { const id = params.id as string; const origin = new URL(request.url).origin; const targetUrl = `${origin}/receipt/${id}`; const cache = locals.runtime.env.PDF_CACHE;
// Try KV cache first to avoid unnecessary browser work const cachedPdf = await cache.get(targetUrl, "arrayBuffer"); if (cachedPdf) return pdfResponse(Buffer.from(cachedPdf), id);
// Only run browser in Cloudflare's env if (!locals.runtime.env.BROWSER) return new Response('Browser binding not available locally', { status: 501 });
// Launch browser and render HTML to PDF const session = await puppeteer.launch(locals.runtime.env.BROWSER); const page = await session.newPage(); await page.goto(targetUrl, { waitUntil: 'networkidle0' }); const pdf = await page.pdf({ format: 'A4', printBackground: true, margin: { top: '24mm', right: '18mm', bottom: '24mm', left: '18mm' } }); await session.close();
const result = Buffer.from(pdf); // Cache for a week (604800 seconds) await cache.put(targetUrl, result, { expirationTtl: 60 * 60 * 24 * 7 }); return pdfResponse(result, id);};Now, when you hit /api/pdf/123, you’ll receive a PDF generated live from /receipt/123. Subsequent requests return the cached PDF until it expires.
Protect and cache your PDFs
- Auth: Require a session token or signed query string before rendering. Reject unauthenticated requests early.
- Caching: Cache static invoices for short periods (
Cache-Control: public, max-age=300) to avoid re-rendering. For dynamic content, keep cache private. - Rate limits: Add a rate limiter (KV) for high-traffic endpoints.
Deploy to Cloudflare Workers
Deploy your PDF generation application to production:
# Build the projectnpm run build
# Deploy to Cloudflare Workersnpx wrangler deployAfter deployment, your API will be live at https://your-worker.workers.dev/api/pdf/[id].
Conclusion
Cloudflare Browser Rendering transforms PDF generation from a resource-heavy operation into a lightweight edge function. By combining Astro’s SSR capabilities with headless Chromium at the edge, you can generate invoices, receipts, and reports on-demand without managing infrastructure or dealing with cold starts.
For advanced use cases, explore multi-page documents, custom headers/footers, or even screenshot generation for Open Graph images. The Browser Rendering API gives you full Puppeteer control, all running at Cloudflare’s edge.
If you have any questions or comments, feel free to reach out to me on Twitter.