Why Your PDF Pipeline Is Slower Than It Needs To Be

Published: (March 2, 2026 at 04:09 AM EST)
4 min read
Source: Dev.to

Source: Dev.to

Every backend engineer has been there. The client wants invoices, reports, certificates, statements. “Just generate a PDF,” they say, like it’s a print() statement.

So you reach for the standard toolchain: render HTML with Jinja2, spin up a headless Chrome instance, call page.pdf(), and pray it doesn’t OOM on the 500th document in the batch.

It works. Until it doesn’t.

The headless browser tax

Here’s what actually happens when you generate a PDF through a headless browser:

  1. Spawn a Chromium process (or connect to a pool)
  2. Create a new page context
  3. Load your HTML + CSS + assets
  4. Wait for fonts, images, layout
  5. Call the print‑to‑PDF API
  6. Serialize the PDF bytes
  7. Tear down the page context

For a single invoice this takes 2–5 seconds. For a batch of 10 000 monthly statements you’re looking at hours of compute, gigabytes of RAM, and a deployment that needs its own dedicated infrastructure just to print documents.

The worst part? Chromium is rendering a full web page—JavaScript engine, DOM, CSSOM, layout tree, paint, composite—when all you need is “put these words in these positions and draw some lines.”

What deterministic rendering actually means

A deterministic PDF renderer doesn’t interpret your document as a web page. It reads a structured template, resolves the layout, and writes PDF primitives directly. No browser. No JavaScript engine. No intermediate rendering steps.

Comparison

MetricHeadless BrowserNative Renderer
Time per document2–5 seconds50–200 ms
Memory per document200–500 MB10–30 MB
Batch of 10 0005–14 hours8–30 minutes
DeterminismNo (race conditions)Yes (SHA‑256 reproducible)
DependenciesChrome/PuppeteerNone

That last row matters more than you think. “No dependencies” means your PDF generation works in a Lambda function, a Docker container, a CI pipeline, or a bare‑metal server with nothing installed. No Chrome binary to manage. No version mismatches. No sandboxing headaches.

The reproducibility problem nobody talks about

Generate the same invoice twice with a headless browser. Compare the bytes. They won’t match.

  • Fonts render slightly differently across runs.
  • Timestamps embed in metadata.
  • Image compression isn’t bitwise stable.

This means you can’t verify a document hasn’t been tampered with by comparing hashes, you can’t cache aggressively, and you can’t build audit trails that depend on document identity.

A deterministic renderer produces identical bytes for identical inputs—always. This isn’t academic; it’s a compliance requirement in healthcare, finance, and legal document pipelines. If you’re generating documents for regulated industries, non‑determinism is a liability.

What this looks like in practice

I built Fullbleed to solve this problem. It’s a Rust‑native PDF rendering engine with Python bindings.

Installing and rendering a single document

pip install fullbleed
fullbleed render invoice.html --output invoice.pdf

Batch rendering

from fullbleed import render_batch

documents = [
    {"template": "invoice.html", "data": customer}
    for customer in customers
]

# Renders across all available cores via Rayon
results = render_batch(documents, workers="auto")

10 000 invoices. Minutes, not hours. Deterministic output. No Chrome in sight.

When you should (and shouldn’t) use a native renderer

Use a native renderer when:

  • You’re generating documents in batch (invoices, statements, reports)
  • You need reproducible output for compliance or auditing
  • Your pipeline runs in constrained environments (Lambda, CI, edge)
  • Performance matters (sub‑second per document)
  • You want zero external dependencies

Stick with headless Chrome when:

  • You’re rendering arbitrary user‑provided HTML/CSS/JS
  • You need pixel‑perfect web page screenshots
  • Your templates use complex JavaScript interactions
  • You generate fewer than 10 documents per day and don’t care about speed

Most backend engineers default to headless Chrome because it’s what they know. But if your use case is structured document generation—templates with data—you’re paying a massive performance and complexity tax for capabilities you don’t need.

The takeaway

PDF generation is a solved problem that most teams solve badly. Not because they’re incompetent, but because the obvious tools (wkhtmltopdf, Puppeteer, Playwright) optimize for generality over performance. When your actual requirement is “fill this template with this data and give me a PDF,” a purpose‑built renderer is 10–100× faster, uses a fraction of the memory, and produces deterministic output.

If you’re building document pipelines and want to explore this approach, Fullbleed is open source (AGPLv3) and installs with pip install fullbleed. Commercial licenses are available for proprietary use.

0 views
Back to Blog

Related posts

Read more »

The 185-Microsecond Type Hint

How a “trivial” change yielded a 13× throughput increase We recently releasedhttps://blog.sturdystatistics.com/posts/roughtime/ an open‑source Clojure implemen...