Inkspan › Guides › wkhtmltopdf

wkhtmltopdf alternatives in 2026: a benchmarked migration guide

Last updated 29 July 2026 · benchmarks re-run on each update · methodology

Every replacement for wkhtmltopdf makes a trade you should understand before you start rewriting templates. This guide measures four of them on the same document and tells you which fits — including the cases where the right answer is free software rather than a paid product.

Short answer

If your HTML needs JavaScript, use a Chromium-based renderer — Playwright or Puppeteer as a library, or Gotenberg as a self-hosted HTTP service. If your HTML is server-rendered and static — which most invoices, statements and reports are — WeasyPrint is faster, lighter and free. wkhtmltopdf executed JavaScript, so moving to a non-browser engine can be a feature regression. Check that first; it decides everything else.

Why you have to move

FactDetail
Last release0.12.6, 10 June 2020
RepositoryArchived and read-only; the packaging repo was archived 28 August 2023
EngineA patched Qt WebKit fork, years behind modern browsers on CSS
DebianRemoved for Trixie — bug #1091821, "RoQA; upstream dead; depends on qtwebkit". Still in bullseye and bookworm.
Open CVECVE-2022-35583 — SSRF, CVSS v3.1 9.8, no upstream fix
Sources linked inline. Verified 29 July 2026.
Worth being precise about the CVE

Most comparisons lead with "critical 9.8 vulnerability" and stop. The fuller picture: Ubuntu's security team deferred the fix and attributes the issue to applications passing unsanitised user-supplied HTML to the renderer, not to a defect in wkhtmltopdf itself.

That matters twice. If you only ever render your own templates, your exposure is far lower than the score implies. And if you do render untrusted HTML, this class of SSRF follows you to whichever engine you choose — any renderer that fetches remote subresources can be pointed at your internal network. Migrating does not fix it; restricting the renderer's network egress does.

The strongest honest reason to move is simpler: an unmaintained engine with no security patches, dropped by your distro, that will eventually stop building.

The question that decides your migration

wkhtmltopdf ran JavaScript. Qt WebKit was a real browser engine, so --javascript-delay and --window-status existed and people relied on them: charts drawn client-side, tables populated after load, templates waiting on a fetch. Half the replacement options cannot do this at all.

Run one test before choosing anything. Render this and read the output:

<p id="out">JS_DID_NOT_RUN</p>
<script>document.getElementById('out').textContent = 'JS_DID_RUN';</script>

Your PDF says JS_DID_RUN

You need a browser engine:

Skip the WeasyPrint sections below; they will not work for you.

Your PDF says JS_DID_NOT_RUN

Your templates are server-rendered — the common case for invoices, statements, contracts and reports. Every option is open to you, and the lightest ones are the best:

Expect reflow either way

No replacement paginates identically to Qt WebKit. Teams report the same document breaking across a different number of pages after switching. Budget time to adjust @page rules and page-break CSS on your busiest templates, and diff output page-by-page before shipping.

The options, side by side

OptionEngineJavaScriptRuns asBest fit
WeasyPrintOwn CSS layout engine (Python)NoLibraryServer-rendered documents; strong @page / paged-media support
Playwright / PuppeteerChromiumYesLibrary + browser binaryJS-dependent pages; closest functional match to wkhtmltopdf
GotenbergChromiumYesDocker HTTP serviceSeveral apps sharing one renderer; keeps Chromium out of your app image
TypstPurpose-built typesetterNoBinary / libraryHigh volume or typography-critical output; not an HTML drop-in
Commercial CSS enginesProprietary (e.g. Prince)VariesLicensed binary or APIPrint-grade pagination, PDF/A, complex page rules
Hosted APIsVaries — askVaries — askHTTP callNo renderer to install, patch or scale
"Ask" is not a hedge — hosted engines differ, and the engine decides whether your JS-dependent template works at all.

Benchmarks on the same document

Comparisons of these tools are usually prose. Here are numbers. One realistic A4 invoice — eight line items, CSS grid, flexbox, striped table, @page footer with page counters — rendered five times per path, median reported.

PathMedianOutputNotes
Chromium, warm browser71 ms42.4 KBFastest by a wide margin — if you keep a browser alive
WeasyPrint, in-process182 ms17.3 KBFree; 651 ms one-time import cost
Hosted API (Inkspan)871 ms17.3 KBIncludes network round-trip from the test machine
Chromium, cold process1,499 ms42.4 KBFresh process per run — proxy for a serverless cold start
Five iterations, median. Single Linux VM, Chromium 150 via Playwright, WeasyPrint 65.1. Not a controlled benchmark rig — see methodology.

What the numbers actually say

Flag-by-flag migration map

wkhtmltopdf flagWeasyPrintChromium (Playwright / Puppeteer)
--page-size A4@page { size: A4 }format: 'A4'
--orientation Landscape@page { size: A4 landscape }landscape: true
-T -B -L -R margins@page { margin: ... }margin: { top, bottom, left, right }
--header-html / --footer-htmlMargin boxes: @top-center, @bottom-centerheaderTemplate / footerTemplate + displayHeaderFooter
[page] / [topage]counter(page) / counter(pages)<span class="pageNumber"> / totalPages
--javascript-delay, --window-statusNo equivalent — no JS executionwaitUntil, waitForSelector, waitForFunction
--enable-local-file-accessbase_url argumentServe assets over HTTP, or file:// with care
--print-media-typePrint media is the defaultemulateMedia({ media: 'print' })
--zoomCSS sizing / zoomscale
--cookie, --custom-headerFetch the HTML yourself, pass the stringsetExtraHTTPHeaders, context.addCookies
--tocRebuild with target-counter in CSSGenerate in your template
--grayscale, --lowqualityPost-process the PDFPost-process the PDF
Where a flag has no equivalent, that is stated rather than glossed over.

WeasyPrint, minimal version

from weasyprint import HTML

pdf = HTML(string=html, base_url="./assets/").write_pdf()

Playwright, minimal version

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(args=["--no-sandbox"])
    page = browser.new_page()
    page.set_content(html, wait_until="load")
    pdf = page.pdf(format="A4", print_background=True)
    browser.close()   # reuse the browser across requests in production

The real cost of self-hosting a browser

wkhtmltopdf shipped one binary. Every Chromium-based replacement ships an operations problem. This is not an argument against self-hosting — a warm Chromium was the fastest option we measured — but the costs are real and rarely listed:

WeasyPrint avoids nearly all of this — no browser, no binary matrix — at the cost of no JavaScript. Its own friction is the Pango and cairo system libraries: unremarkable on Linux, genuinely annoying on Windows and in slim containers.

If you'd rather not run a renderer

A hosted API is the right call when rendering isn't your product, your traffic is bursty enough that cold starts dominate, or your platform makes the dependencies painful. Options worth comparing, and the two questions to ask each: what engine, and does it execute JavaScript?

ServiceNotes
Gotenberg (self-host or hosted)Chromium; open source, so you can start hosted and move in-house without changing engine
DocRaptorCommercial CSS engine; aimed at print-grade pagination
PDFShift, Api2Pdf, BrowserlessBrowser-based; confirm current engine and JS support before committing
PDFSparkChromium via Playwright; free no-signup tier with per-IP rate limits
InkspanWeasyPrint 65.1; no JavaScript execution; free tier of 100 documents/month
Listed because they are the real alternatives. Verify specifics yourself — details change.

Disclosure, and the part most vendor guides leave out. This guide is published by Inkspan. Our /v1/generate/html endpoint is hosted WeasyPrint 65.1 — you can verify that yourself in the /Producer field of any PDF we return. So we do not execute JavaScript, and we are not a replacement for Puppeteer or for the JS-dependent parts of a wkhtmltopdf setup. It also means that if you can install WeasyPrint locally, you get the same rendering for free and faster — our own benchmark above says so. What you buy from us is not the renderer: it is not owning the dependencies, and the pipeline below.

The rest of the pipeline nobody mentions

Migration guides treat this as an HTML-to-PDF problem. Real wkhtmltopdf deployments rarely were. If yours also shells out to pdftk, qpdf, Ghostscript or wkhtmltoimage, those are separate migrations you haven't scoped — and pdftk has its own abandonment story.

Audit for these before you estimate the work:

You can assemble all of it from open-source parts — pypdf and qpdf cover most cases well. The reason to consider a single API is that each is a separate dependency, with separate system libraries and a separate migration the next time one gets archived. Inkspan exposes them as one authenticated POST each (/pdf/merge, /pdf/split, /pdf/compress, /pdf/watermark, /pdf/to-images, /pdf/fill-form, /extract/text, /extract/tables) — a convenience argument rather than a capability one. Both are legitimate reasons to buy; they are just different reasons.

A note on AI agents

One workload where the no-JavaScript trade-off costs nothing: documents generated by LLMs and agents. An agent producing an invoice, report or summary emits Markdown or server-side HTML. It is never rendering a single-page app, so a browser engine buys it nothing but latency and footprint.

For that shape of work the useful primitives are Markdown or template-plus-JSON to PDF, and extraction and translation on the way back in — not waitForSelector. If you are building document generation into an agent pipeline, evaluate on those, and don't pay for a browser you will never use.

Buying time: the maintained fork

If the migration is real work and you need a quarter to plan it properly, there is a middle option the other guides don't mention. newinnovations/wkhtml-packaging continues to publish wkhtmltopdf 0.12.6.1 builds for current distributions, including Debian 13 and Ubuntu 26.04, with releases as recent as May 2026.

That is a packaging fork, not a maintained engine: it does not modernise Qt WebKit and it does not fix the SSRF class. Use it to unblock a build, on trusted templates only, with a migration date on the calendar. It is a bridge, not a destination.

FAQ

Is wkhtmltopdf still safe to use on my own templates?
Lower risk than the CVSS 9.8 headline implies if you never render user-supplied HTML and block the renderer's outbound network access. It is still an unpatched engine dropped by Debian, so treat continued use as deliberately accepting debt with a deadline.
What is the closest drop-in replacement?
Chromium via Playwright or Puppeteer. It is the only common option that keeps JavaScript execution, which is the capability most likely to break silently.
Will my PDFs look the same afterwards?
No. Pagination changes on any engine you move to. Diff your busiest templates page by page and expect to adjust page-break CSS.
Is WeasyPrint good enough for invoices?
Yes, and arguably better suited than a browser. Paged media — margin boxes, page counters, running headers — is what it was built for. The constraint is JavaScript, which invoices rarely need.
Do I need a paid API at all?
Often not. If pip install weasyprint works on your platform and your HTML is static, use the library. Consider hosted when the dependencies are painful, cold starts dominate your traffic, or you want the operations to be someone else's problem.
Which is fastest?
A warm Chromium, by roughly an order of magnitude, provided you keep the process alive. On cold starts that reverses and a warm remote renderer wins.

Methodology

What was measured. One A4 invoice with eight line items, CSS grid, flexbox, a striped table and an @page footer using page counters. Five iterations per path, median reported, identical input HTML throughout. Chromium 150 driven by Playwright; WeasyPrint 65.1; hosted call to POST /v1/generate/html.

What these numbers do not prove. They come from a single Linux VM, not a controlled benchmark rig, and one document is not a representative corpus — heavy documents with many images or webfonts will shift the ordering. The hosted figure includes network round-trip from the test machine, so your latency depends on your distance from the API. The "cold process" row launches a fresh OS process per iteration as a stand-in for a serverless cold start; it is not a measurement of AWS Lambda. Treat all of it as an order-of-magnitude guide and re-run on your own documents before making a decision that matters.

Changelog

Get an API key →

Related: generate a PDF from HTML in Python · template to PDF · invoice PDF API · merge PDF files