<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Keenan Finkelstein</title>
    <description>The latest articles on DEV Community by Keenan Finkelstein (@krflol).</description>
    <link>https://dev.to/krflol</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3800996%2Fa880f0b9-cd95-4f71-848d-5bb376d47b78.png</url>
      <title>DEV Community: Keenan Finkelstein</title>
      <link>https://dev.to/krflol</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/krflol"/>
    <language>en</language>
    <item>
      <title>Why Your PDF Pipeline Is Slower Than It Needs To Be</title>
      <dc:creator>Keenan Finkelstein</dc:creator>
      <pubDate>Mon, 02 Mar 2026 09:09:47 +0000</pubDate>
      <link>https://dev.to/krflol/why-your-pdf-pipeline-is-slower-than-it-needs-to-be-med</link>
      <guid>https://dev.to/krflol/why-your-pdf-pipeline-is-slower-than-it-needs-to-be-med</guid>
      <description>&lt;p&gt;Every backend engineer has been there. The client wants invoices. Reports. Certificates. Statements. "Just generate a PDF," they say, like it's a &lt;code&gt;print()&lt;/code&gt; statement.&lt;/p&gt;

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

&lt;p&gt;It works. Until it doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  The headless browser tax
&lt;/h2&gt;

&lt;p&gt;Here's what actually happens when you generate a PDF through a headless browser:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Spawn a Chromium process (or connect to a pool)&lt;/li&gt;
&lt;li&gt;Create a new page context&lt;/li&gt;
&lt;li&gt;Load your HTML + CSS + assets&lt;/li&gt;
&lt;li&gt;Wait for fonts, images, layout&lt;/li&gt;
&lt;li&gt;Call the print-to-PDF API&lt;/li&gt;
&lt;li&gt;Serialize the PDF bytes&lt;/li&gt;
&lt;li&gt;Tear down the page context&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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."&lt;/p&gt;

&lt;h2&gt;
  
  
  What deterministic rendering actually means
&lt;/h2&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The difference in practice:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Headless Browser&lt;/th&gt;
&lt;th&gt;Native Renderer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Time per document&lt;/td&gt;
&lt;td&gt;2-5 seconds&lt;/td&gt;
&lt;td&gt;50-200ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Memory per document&lt;/td&gt;
&lt;td&gt;200-500 MB&lt;/td&gt;
&lt;td&gt;10-30 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Batch of 10,000&lt;/td&gt;
&lt;td&gt;5-14 hours&lt;/td&gt;
&lt;td&gt;8-30 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Determinism&lt;/td&gt;
&lt;td&gt;No (race conditions)&lt;/td&gt;
&lt;td&gt;Yes (SHA256 reproducible)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dependencies&lt;/td&gt;
&lt;td&gt;Chrome/Puppeteer&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reproducibility problem nobody talks about
&lt;/h2&gt;

&lt;p&gt;Generate the same invoice twice with a headless browser. Compare the bytes. They won't match.&lt;/p&gt;

&lt;p&gt;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. You can't build audit trails that depend on document identity.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this looks like in practice
&lt;/h2&gt;

&lt;p&gt;I built &lt;a href="https://github.com/fullbleed-engine/fullbleed-official" rel="noopener noreferrer"&gt;Fullbleed&lt;/a&gt; to solve this problem. It's a Rust-native PDF rendering engine with Python bindings. Here's what generating a document looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;fullbleed
fullbleed render invoice.html &lt;span class="nt"&gt;--output&lt;/span&gt; invoice.pdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under the hood: Rust parses the HTML/CSS, resolves layout using its own engine (no browser), and writes PDF 1.7 directly. Python bindings release the GIL during Rust execution, so you can parallelize across cores in a batch job.&lt;/p&gt;

&lt;p&gt;For batch rendering:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fullbleed&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;render_batch&lt;/span&gt;

&lt;span class="n"&gt;documents&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;template&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;invoice.html&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;customers&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# Renders across all available cores via Rayon
&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;render_batch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;documents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;workers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;auto&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;10,000 invoices. Minutes, not hours. Deterministic output. No Chrome in sight.&lt;/p&gt;

&lt;h2&gt;
  
  
  When you should (and shouldn't) use a native renderer
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Use a native renderer when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're generating documents in batch (invoices, statements, reports)&lt;/li&gt;
&lt;li&gt;You need reproducible output for compliance or auditing&lt;/li&gt;
&lt;li&gt;Your pipeline runs in constrained environments (Lambda, CI, edge)&lt;/li&gt;
&lt;li&gt;Performance matters (sub-second per document)&lt;/li&gt;
&lt;li&gt;You want zero external dependencies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Stick with headless Chrome when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're rendering arbitrary user-provided HTML/CSS/JS&lt;/li&gt;
&lt;li&gt;You need pixel-perfect web page screenshots&lt;/li&gt;
&lt;li&gt;Your templates use complex JavaScript interactions&lt;/li&gt;
&lt;li&gt;You generate fewer than 10 documents per day and don't care about speed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;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-100x faster, uses a fraction of the memory, and produces deterministic output.&lt;/p&gt;

&lt;p&gt;If you're building document pipelines and want to explore this approach, &lt;a href="https://github.com/fullbleed-engine/fullbleed-official" rel="noopener noreferrer"&gt;Fullbleed is open source&lt;/a&gt; (AGPLv3) and installs with &lt;code&gt;pip install fullbleed&lt;/code&gt;. Commercial licenses are available for proprietary use.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm Keenan, a backend engineer specializing in Python, Rust, and document automation. I built Fullbleed because I got tired of managing Chromium clusters just to print invoices. If you're working on a document pipeline and want to talk architecture, &lt;a href="https://www.upwork.com/freelancers/~01fe1ab70d555a8b4c" rel="noopener noreferrer"&gt;find me on Upwork&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>performance</category>
    </item>
  </channel>
</rss>
