Back to blogs
Engineering·June 25, 2026 · 3 min read

What Actually Happens When You Convert HTML to PDF at Scale

A single test conversion looks perfect. The header renders, the footer sits where you expect it, the fonts look right. Then you run ten thousand real documents a day through the same html to pdf pipeline, and everything you didn't test shows up at once.

Fonts are the first thing to break

Headless Chromium can render fonts inconsistently, sometimes blotchy or misaligned in ways that never show up in a regular browser tab. Part of the fix is blunt: setting the font-render-hinting flag to none cleans up a lot of the kerning and spacing problems people run into with Puppeteer-based rendering.

There's a subtler failure too. Some rendering setups need a legitimate user-agent string, or the request for a web font gets treated as suspicious and silently ignored, so the PDF falls back to a system font instead. Nobody notices in a demo where the sample document uses a generic sans-serif. It becomes obvious the moment a real invoice goes out with the wrong typeface and a client asks why your branding looks off.

Embed fonts directly in the HTML you send for conversion where you can, and confirm the rendering environment actually has access to fetch any font it doesn't already have installed.

Margins and page breaks don't work the way you'd guess

Most PDF rendering engines, Puppeteer included, ignore your CSS body margins when printing. If you've set margin on body and wondered why the PDF output ignores it, that's why. Print-safe margins need to be defined with an @page rule instead, which is easy to miss the first time you hit it.

Page breaks inside tables cause a related problem. Browsers are forgiving about where content overflows a viewport. PDF renderers are not, and a table row that spans a page boundary can get cut in half or pushed onto a mostly blank page depending on how the engine handles break-inside rules.

  • Define explicit page-break CSS on tables and long sections instead of relying on defaults
  • Set margins with @page, not body, since most engines ignore the latter during print
  • Test with a document that's deliberately close to a page boundary, not one that fits neatly

Rounding errors show up as gaps you can't explain

A few teams have reported horizontal gaps appearing on the page after conversion, traced back to rounding differences between the requested page size and what the renderer actually produces. It's a small mismatch, often a pixel or two, but it's the kind of bug that looks like a rendering glitch until you check the exact width and height values you're passing in.

Infrastructure fails quietly before it fails loudly

Running headless Chromium requires a specific set of system libraries and font packages. Without them, it doesn't always throw a clear error, it can crash or behave inconsistently depending on what's missing. This is the failure mode that looks fine in local development, where your machine already has every font and library installed, and only appears once you deploy to a stripped-down container image.

Concurrency changes the failure mode again. A rendering engine that handles one request cleanly can behave differently under load: shared browser instances, memory pressure, and connection pool exhaustion all show up only once you're running real volume. Load test with the concurrency level you'll actually see in production, not the number that's convenient to run on your laptop.