Comparisons

Convert HTML to PDF: six methods, and how each one breaks

Browser print dialog, wkhtmltopdf, WeasyPrint, Puppeteer/Playwright, LibreOffice, and a hosted API. What each produces, what it costs to run, and the failure you will hit first.

M Mikel Rougstone · 31 August 2026 · 5 min read
Convert HTML to PDF: six methods, and how each one breaks

There are more ways to turn HTML into a PDF than there are good reasons to. Six of them are worth knowing, each fails differently, and the right choice depends less on features than on who is going to keep the thing running six months from now. Here is the honest map.

The short version

MethodFidelitySetupJavaScriptBest for
Browser print dialogPerfectNoneYesOne document, by hand
wkhtmltopdfPoor on modern CSSBinary + libsPartial, old engineLegacy systems only
WeasyPrintExcellent for documentspip installNoInvoices, contracts, statements
Puppeteer / PlaywrightPerfectBrowser + opsYesFull control, you own the fleet
LibreOffice headlessPoorHeavy installNoOffice formats, not HTML
Hosted APIPerfectOne HTTP callYesEverything else

1. The browser's print dialog

Open the page, press Ctrl+P, choose *Save as PDF*. Free, perfect fidelity, and zero automation.

It is genuinely the right answer when someone needs one document once. The moment a second person needs it, or a cron job does, it stops being an answer at all. There is no API, no way to pass data, and no way to guarantee two people produce the same file — margins and the "background graphics" checkbox differ per machine.

Breaks when: you need it done more than once, or by anyone but a human.

2. wkhtmltopdf

A command-line tool wrapping a fork of WebKit. For a decade it was the default answer in every Stack Overflow thread, and it still appears in Dockerfiles everywhere.

The project is archived. The engine underneath predates the CSS most teams write today: flexbox behaves oddly, grid is absent, and several properties are ignored rather than approximated. The symptom is always the same — a layout that is correct in the browser and collapses in the PDF.

There is also an operational cost people forget: it is a binary on every machine that renders, with system libraries it expects to find, and an unpatched rendering engine sitting inside your infrastructure.

Breaks when: your CSS is younger than about 2016, or web fonts need to load reliably. See what to use instead and the migration checklist.

3. WeasyPrint

A Python library that implements the paged-media parts of CSS and writes PDF directly — no browser involved. For documents *authored as documents*, the output is often better than a browser's: running headers, page counters, footnotes and string-set all behave the way the specification says.

It runs no JavaScript at all. A page that mounts its content with a framework arrives empty.

In our own measurements WeasyPrint renders a one-page invoice in about 0.19 s against 0.64 s for a warm Chromium — three times faster, because there is no browser in the path. On a fourteen-page report with charts the order flips. The full numbers are in WeasyPrint vs Playwright.

Breaks when: the page needs a script to render anything.

4. Puppeteer or Playwright

Drive real headless Chromium and call page.pdf(). Output matches the browser exactly, because it *is* the browser.

The cost is operational, and it arrives later than you expect:

  • Chromium and its system libraries on every rendering machine — roughly 400 MB in your image
  • A warm browser between renders; a cold start costs over a second
  • A pool, because one browser per request will bury a small server
  • Recycling, because a long-lived browser that has rendered thousands of pages holds more memory

than one that has rendered five

  • Timeouts and crash recovery, because one page that never fires load should not stall a queue

None of it is difficult. All of it is yours to run.

Breaks when: you deploy to a 512 MB container and discover Chromium wants most of it before it renders anything.

5. LibreOffice headless

soffice --headless --convert-to pdf file.html. Surprisingly serviceable for plain HTML, poor for anything styled, and a heavy install. It exists in this list because people try it — usually because LibreOffice is already present for DOCX conversion.

Breaks when: CSS matters at all.

6. A hosted API

You POST HTML, you receive a PDF. Someone else runs the browser, patches it, pools it, and pays for its memory.

curl -X POST https://pdfgeny.com/api/v1/render \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"html":"<h1>Invoice 001</h1><p>Total: €149</p>","format":"A4"}' \
  --output invoice.pdf

That is the entire integration. The trade is a network hop and a per-document price; what you get back is not having a browser in your deployment.

Breaks when: you have no outbound network, or your volume is high enough that a dedicated box is cheaper than per-document pricing.

Choosing, honestly

  • One document, once → print dialog. Do not build anything.
  • Documents you author yourself, no JavaScript, hundreds a day → WeasyPrint. It is fast, cheap

and the typography is excellent.

  • Real web pages, or output that must match the browser, and you have people to run infrastructure

→ Puppeteer or Playwright.

  • Real web pages, and you would rather not run a browser fleet → a hosted API.
  • You are on wkhtmltopdf today → move. Not urgently, but deliberately; the engine will not improve.
A useful test: if the PDF is your product, own the rendering. If the PDF is a feature of your
product, rent it.

What actually breaks, in order of frequency

1. Fonts fall back. The PDF shows Times where the page showed your brand font. Causes are timing, CORS, and formats — all covered in web fonts in generated PDFs. 2. Page breaks land badly. Table rows split, headings orphan at the bottom. Four CSS properties fix most of it: see page breaks in HTML to PDF. 3. Backgrounds disappear. Almost always print_background (or the browser dialog's checkbox) left off. 4. The page is empty. Rendering started before JavaScript finished. Wait for network idle, or for a selector that only exists once content is drawn. 5. Memory grows. A long-lived browser leaks slowly. Recycle it every few hundred documents.

Try it without installing anything

Paste markup into the HTML to PDF converter and download the result, or point the URL to PDF converter at a page. Both use the same Chromium the API does, so what you see there is what the API returns.

M

Mikel Rougstone

Founder, PDFGeny

I build and run PDFGeny — the API, the rendering fleet and the template catalog. Most of what I write here comes from something that broke in production first.