Disclosure up front: I'm Vitalii, founder of PDFik, a hosted
URL/HTML-to-PDF API. It appears below as one option among several — including the ones that
compete with it. Where a competitor is the better fit, I say so.
For a decade, wkhtmltopdf was the default answer to "how do I turn HTML into a PDF from my
backend?" — one static binary, no browser to babysit, a flag for everything. Most invoice,
report and ticket generators older than five years have it somewhere in their dependency tree.
Three facts changed that:
- The project is archived. The GitHub repository was archived on January 2, 2023 and is read-only: no maintainers, no releases, no security patches — ever.
- There is an unpatched vulnerability. CVE-2022-35583 (SSRF via rendered content) will never be fixed upstream. If your scanner flags it, there is no "upgrade to version X" remediation — the remediation is migration.
- The packaging is rotting. The engine is a patched Qt WebKit from another era: modern CSS (grid, flexbox gaps, custom properties) silently misrenders, distros have been dropping the package, official builds don't cover current Debian/Ubuntu releases or arm64 well, and Docker images increasingly rely on third-party rebuilds of an unmaintained binary.
None of this means your PDFs stop rendering tomorrow. It means every new deployment target,
every security audit, and every CSS feature your designers use widens the gap. Here is the
decision tree I'd use to close it.
First, inventory what wkhtmltopdf actually does for you
Before comparing engines, grep your codebase for the flags you really use. In practice most
setups boil down to a handful:
| You use | You need from a replacement |
|---|---|
--header-html / --footer-html, page numbers |
header/footer templates with page variables |
--margin-*, --orientation, --page-size
|
standard page options (every option below has these) |
--print-media-type |
print-CSS support (@page, page-break-*) |
--javascript-delay, --window-status
|
an explicit "wait until ready" mechanism |
--cookie, --custom-header
|
authenticated fetching of the source page |
--toc, --outline
|
PDF outline/bookmarks — check carefully, this is the weakest spot everywhere |
| stdin/stdout piping in a worker | either a library call or an API client |
Two honest warnings apply to every path below: no Chromium-based engine reproduces
wkhtmltopdf output pixel-for-pixel (different engine, different line breaking, different
font fallback — you will re-approve golden files), and --toc/--outline have no
first-class Chromium equivalent (you rebuild outlines with a post-processing step or live
without them).
Path 1 — in-process library: WeasyPrint
If your documents are print-oriented (invoices, contracts, statements), your templates don't
run JavaScript, and you're on Python — WeasyPrint is the closest
thing to a spiritual successor: a real @page-first CSS engine, no browser process at all,
excellent page-break control, actively maintained.
- Choose it when: print CSS is enough, no JS charts, Python stack, documents must not leave your infrastructure.
- Walk away when: templates depend on JavaScript rendering (Chart.js, React-rendered markup) or you need the page to look exactly like it does in a browser.
The commercial sibling of this category is PrinceXML (and DocRaptor,
the hosted API built on it) — the strongest print-CSS engine on the market and the reference
choice when compliance-grade, print-perfect documents justify the price.
Path 2 — DIY headless Chromium: Playwright or Puppeteer
page.pdf() in Playwright / Puppeteer gives
you a modern engine, full JS execution, and total control. The code is five lines; the
operations are not: you now run a browser fleet. Zombie processes, memory ceilings per tab,
crash-looping renderers under load, sandboxing (seccomp/user namespaces if you take isolation
seriously), font packages in the image, timeouts, and scaling the pool — all yours.
- Choose it when: you already operate containers comfortably, render volume is modest or bursty-but-internal, and you want zero per-document vendor cost.
- Walk away when: PDF generation is a side feature and you'd rather not own a browser farm's pager duty.
Path 3 — self-hosted rendering API: Gotenberg
Gotenberg wraps Chromium (and LibreOffice for office formats) in a
Docker container with a clean HTTP API. You keep data on your infrastructure and get out of
the "manage Playwright yourself" business; you still own capacity planning, upgrades and
availability.
- Choose it when: documents must stay in your VPC but you want an API, not a library.
- Walk away when: you don't want to run and scale the container at all.
Path 4 — hosted APIs
If rendering is not your core business, a hosted API turns the whole problem into an HTTP
call. The market is healthy — a few honest reference points, all with published pricing:
- DocRaptor — PrinceXML engine, the print-CSS gold standard, SOC 2 / HIPAA-BAA posture; documents can be very large. The established choice for compliance-heavy document generation.
- PDFShift, Api2Pdf, PDFMonkey and others — Chromium-based hosted rendering with different pricing shapes (per-document, per-credit, template-first workflows). Worth shortlisting all three; which wins depends on your volume curve.
-
PDFik — my product, so discount accordingly. The design bet is
async-first: you
POSTa URL or HTML, get ajob_id, and receive an HMAC-signed webhook when the file is ready (polling and downloads exist too), with a free test mode that runs the full pipeline without touching your quota. There's a flag-by-flag wkhtmltopdf migration map that covers the table above in detail — including the places where wkhtmltopdf still wins. If what you actually want is to keep your existing wkhtmltopdf command lines, see path 5 below.
When is a hosted API simply wrong? Air-gapped environments, documents that legally may not
leave your infrastructure, or rendering volumes so high that per-document pricing can't beat
your marginal server cost. Those cases belong to paths 1–3.
Path 5 — keep the wkhtmltopdf interface, swap the engine: pdfik wkhtmltopdf
Full disclosure again: this one is mine too. Sometimes the blocker is not the engine but
the interface — hundreds of lines of shell, cron jobs and wrappers (pdfkit, wicked_pdf)
that all speak wkhtmltopdf's flags, and nobody wants to rewrite them just to retire a
binary. The PDFik CLI ships a compatibility mode that
takes wkhtmltopdf's own command line:
alias wkhtmltopdf='pdfik wkhtmltopdf'
wkhtmltopdf -s A4 -O Landscape --footer-center 'Page [page] of [topage]' https://example.com out.pdf
Every wkhtmltopdf flag is either mapped to the API, accepted with a warning (it has no
effect in this pipeline), or refused with a reason (it would silently change your output)
— never ignored. --version and -h answer the way wrappers expect, so pdfkit and
wicked_pdf keep working unmodified. The CLI itself is one static binary
(Linux/macOS/Windows, MIT, open source), also on Docker as ghcr.io/pdfik/cli; the
flag-by-flag tables live in the repo's
COMPATIBILITY.md.
Be clear about what this is: rendering happens in PDFik's cloud, so this is path 4 wearing
a familiar face — you need network access and an API key, and it is wrong for air-gapped
environments for the same reasons. The engine is sandboxed Chromium, not WebKit, so the
golden-files warning above applies in full. And --toc/--outline are refused, not
emulated — the honest answer from the inventory table stands here too.
- Choose it when: the wkhtmltopdf interface is load-bearing (scripts, wrappers, colleagues' muscle memory) and hosted rendering is acceptable.
- Walk away when: documents may not leave your infrastructure — that is paths 1–3.
The migration checklist (whatever you pick)
- Golden files first. Render your 10 ugliest real documents on the old and new engine, diff visually, and get sign-off before touching production code paths.
-
Fonts. wkhtmltopdf used system fonts; containers and APIs won't have them by accident.
Embed via
@font-faceor install them explicitly, then re-check non-Latin text. -
Page breaks. Replace
page-break-*hacks tuned for WebKit with standardbreak-inside: avoid/@pagerules and re-test tables that span pages. -
Readiness signal. Replace
--javascript-delayguesswork with an explicit wait (a selector,window-status-style flag, or your engine's network-idle event). -
Headers/footers. Rebuild
--header-htmlas the engine's header/footer template and re-verify page numbering. -
Outline/TOC. If you used
--toc, decide now: post-process the PDF to rebuild bookmarks, or drop the feature consciously. - Timeouts and size limits. Async pipelines and APIs enforce both; find your P99 render time and largest document before your users do.
- Keep the old binary in CI for one release as a fallback renderer behind a flag — migrations get reverted for boring reasons.
TL;DR decision tree
- Print-CSS documents, no JS, Python → WeasyPrint (or PrinceXML/DocRaptor when budget allows).
- Want full control and don't mind operating browsers → Playwright/Puppeteer.
- Data must stay home, but you want an API → Gotenberg.
- Want it to be someone else's pager → hosted API (DocRaptor for print/compliance; PDFShift/Api2Pdf/PDFMonkey/PDFik for Chromium-based rendering — pick by pricing shape and workflow; mine is the async/webhook-first one).
- Scripts and wrappers full of wkhtmltopdf flags you'd rather not touch →
pdfik wkhtmltopdf(compatibility mode of my CLI — hosted rendering behind the old interface).
Archive status and CVE status last verified: 2026-08-26. If something above is outdated,
tell me and I'll fix it.
Top comments (0)