wkhtmltopdf alternatives: migrating off an archived renderer
For a decade wkhtmltopdf was the default way to turn HTML into PDF. Libraries in almost every language wrap it: WickedPDF and PDFKit in Ruby, pdfkit in Python, Snappy in PHP, and many Node packages. Its GitHub repository was archived in January 2023 and the project is no longer maintained. If it still runs in your stack, this guide explains what that means, what to replace it with, and how to map your existing flags.
Why move off wkhtmltopdf
- Unmaintained. The repository is archived and read-only. Bugs and security issues in the bundled engine will not be fixed.
- Old WebKit. wkhtmltopdf is built on QtWebKit, a WebKit fork from years ago. It does not support CSS flexbox (only the obsolete
-webkit-boxsyntax), CSS grid, CSS custom properties, or modern JavaScript. Templates designed in today's browsers break in subtle ways. - Packaging pain. Getting the "patched Qt" build with working headers and footers has always been fiddly, and distro packages often lack those patches. Base image upgrades regularly break it.
- Security. It will happily read local files and fetch URLs referenced in the HTML. Newer releases block local file access by default, which is why many projects still pass
--enable-local-file-access, often with user-influenced HTML.
The alternatives
Headless Chromium (Puppeteer, Playwright, Grover, Browsershot)
The most direct replacement. You get Chrome's current rendering engine, so flexbox, grid, web fonts and JavaScript behave as they do in the browser. The cost is operational: Chromium is large, memory hungry, needs system libraries and fonts, and does not fit in serverless bundles without trimmed builds. You also need to manage browser lifecycles, concurrency and timeouts.
Gotenberg
An open-source Docker service that wraps Chromium (and LibreOffice for office files) behind an HTTP API. A good choice if you want self-hosted Chromium rendering without writing the service yourself. You still run, scale and update the container.
WeasyPrint
A Python layout engine with strong CSS Paged Media support and no browser. It does not run JavaScript, and its flexbox and grid support is less complete than Chromium's, but for print-first documents it is excellent and lightweight.
A hosted API
Services like PDFOven run Chromium for you. You send HTML, a URL, Markdown or a template plus data, and receive the PDF. No binaries to ship, nothing to patch.
Mapping wkhtmltopdf flags to PDFOven
Most wkhtmltopdf invocations use a handful of flags. Here is how they translate to the POST /v1/pdf request body:
| wkhtmltopdf | PDFOven |
|---|---|
--page-size A4 |
options.format: "A4" (A3, A4, A5, Letter, Legal, Tabloid) |
-O Landscape |
options.landscape: true |
--margin-top 10mm (and friends) |
options.margin: "10mm" |
--no-background |
options.print_background: false |
--zoom 0.9 |
options.scale: 0.9 (similar effect, not pixel-identical) |
--header-html header.html |
options.header_template: "<div>...</div>" |
--footer-html footer.html |
options.footer_template: "<div>...</div>" |
[page] / [topage] placeholders |
<span class="pageNumber"></span> / <span class="totalPages"></span> |
--javascript-delay 2000 |
wait_for_selector: "#ready" (wait for a real signal instead of a fixed delay) |
--user-style-sheet print.css |
css: "..." |
wkhtmltopdf https://example.com out.pdf |
url: "https://example.com" |
wkhtmltoimage |
POST /v1/image (PNG, or ?type=jpeg) |
Two behavior differences to plan for:
- Header and footer templates are snippets, not pages. wkhtmltopdf loaded a full HTML file for headers and replaced
[page]with JavaScript. Chromium injects header and footer HTML directly and fills the special span classes. They do not inherit your document CSS, so put inline styles (including an explicitfont-size) on the template, and leave enoughmarginfor them. - Layout will improve, and therefore change. Workarounds you added for old WebKit, such as
display: -webkit-boxor table-based columns, still work, but you can now replace them with flexbox and grid. Expect small differences in line breaking and font metrics; compare a few real documents side by side before switching over.
Example: before and after
Before:
wkhtmltopdf --page-size Letter --margin-top 15mm --margin-bottom 15mm \
--footer-center "Page [page] of [topage]" --footer-font-size 8 \
invoice.html invoice.pdf
After:
curl -X POST https://pdfoven.com/v1/pdf \
-H "Authorization: Bearer $PDFOVEN_KEY" \
-H "Content-Type: application/json" \
--data-binary @request.json \
-o invoice.pdf
With request.json:
{
"html": "<!doctype html><html>...</html>",
"options": {
"format": "Letter",
"margin": "15mm",
"print_background": true,
"footer_template": "<div style=\"font-size:8px;width:100%;text-align:center\">Page <span class=\"pageNumber\"></span> of <span class=\"totalPages\"></span></div>"
}
}
In application code you would build that JSON with your language's HTTP client; see the guides for Node.js, Python, PHP and Ruby.
A migration checklist
- Find every call site, including wrapper gems and packages (WickedPDF, PDFKit, Snappy, pdfkit).
- Put the HTML generation behind one function that returns a string, so the renderer is swappable.
- Translate flags using the table above.
- Render a sample of real documents with both engines and compare page counts and breaks.
- Remove
--enable-local-file-accessstyle assumptions: inline CSS and images (or use absolute HTTPS URLs) so the renderer does not need your filesystem. - Delete the wkhtmltopdf binary from your images.
Pricing and limits
PDFOven's free plan includes 100 renders per month, enough to run the comparison above. Paid plans are $12, $39 and $99 for 2,000, 15,000 and 60,000 renders. Errors come back as { "error": { "code", "message" } }, and 429 quota_exceeded means the monthly quota is used. Rendered PDFs are not stored on our servers. To spot-check a template without writing code, use the free HTML to PDF or URL to PDF tools.
Skip the browser plumbing
PDFOven runs Chromium for you. Send HTML, Markdown, a URL or JSON to a template and get a PDF back. 100 free renders every month.
Get a free API key