TL;DR. Use Puppeteer, not wkhtmltopdf, for any new HTML to PDF work in 2026. wkhtmltopdf was archived on GitHub in early 2023 and runs on a frozen ~2014 fork of Qt WebKit, so it cannot render modern CSS (Grid, custom properties) or run modern JavaScript. Puppeteer drives a real headless Chromium, so output matches what Chrome shows. The cost of switching: Puppeteer ships about 300 MB of Chromium and needs a Node.js runtime, while wkhtmltopdf is a single ~50 MB binary. If you have legacy wkhtmltopdf code, plan a migration to a Chromium-based path.
This guide compares the two on the dimensions that decide real projects: rendering engine, CSS and JavaScript support, deployment footprint, and the migration path off wkhtmltopdf.
Quick comparison
| Factor | Puppeteer | wkhtmltopdf |
|---|---|---|
| Status | Actively maintained (Chrome team) | Archived, unmaintained since 2023 |
| Last release | Frequent | 0.12.6 (2020) |
| Rendering engine | Headless Chromium (current) | Qt WebKit fork (~2014, frozen) |
| Modern CSS (Grid, custom properties) | Full | No |
| JavaScript | Full (V8) | Old WebKit JS, unreliable |
| Runtime needed | Node.js | None (static binary) |
| Install size | ~300 MB (Chromium) | ~50 MB (single binary) |
| Security patches | Yes (via Chromium) | None |
| Interface | Node.js API | CLI (plus community wrappers) |
Is wkhtmltopdf deprecated?
Yes. wkhtmltopdf is deprecated and unmaintained. The project archived its GitHub repository in early 2023, and the last stable release (0.12.6) shipped in 2020. An archived repository means no new bug fixes, no security patches, and no compatibility updates.
The maintainers were direct about it: the tool depends on a patched fork of Qt WebKit that is itself long abandoned, and keeping it working against modern operating systems and CSS became unsustainable. The project homepage still serves binaries, but it is a frozen artifact, not a living tool.
For a new project, that settles it. You should not build on a dependency that will never receive another security fix.
What rendering engine does each tool use?
Puppeteer drives current headless Chromium. wkhtmltopdf drives a frozen Qt WebKit fork from around 2014. This single difference explains almost every practical gap between them.
Puppeteer is a Node.js library maintained by the Chrome DevTools team at Google. It controls a real headless Chromium over the Chrome DevTools Protocol, the same engine that renders pages in Chrome. When Chromium gains a CSS feature, Puppeteer renders it. Its page.pdf() method wraps the protocol's print command.
wkhtmltopdf is a command-line tool that links against an old, patched build of Qt WebKit. WebKit moved on years ago, and the fork wkhtmltopdf uses was frozen near 2014. So it renders the web the way a browser did over a decade ago: no CSS Grid, no CSS custom properties, partial and quirky Flexbox, and an old JavaScript runtime.
Does wkhtmltopdf support modern CSS and JavaScript?
Mostly no. wkhtmltopdf cannot render CSS Grid, CSS custom properties (--foo variables), or current Flexbox behavior, and its old WebKit JavaScript engine fails on most modern framework output. Puppeteer renders all of it because it is Chromium.
Concretely, a few things that commonly break under wkhtmltopdf and work under Puppeteer:
- CSS Grid layouts. The frozen WebKit engine has no Grid support, so a grid-based invoice or dashboard collapses.
- CSS custom properties. Design tokens declared as
--brand-colorare ignored, so colors fall back or disappear. - Modern JavaScript. Charts drawn client-side with Chart.js or D3, or any React/Vue component that hydrates in the browser, often render as an empty element. wkhtmltopdf's
--enable-javascriptflag runs the old engine, which chokes on current bundles. - Web fonts and
networkidlewaits. Puppeteer can wait for fonts and async content to load before printing. wkhtmltopdf's timing controls are coarse and date from a different era.
If your HTML is plain CSS 2.1 with no Grid, no custom properties, and no JavaScript, wkhtmltopdf can still produce a correct PDF. That is a shrinking subset of real templates.
How do you generate a PDF with Puppeteer?
Launch a browser, load the HTML, then call page.pdf(). The whole flow is a few lines in Node.js, and the output is a Buffer you can stream or save.
const puppeteer = require('puppeteer');
async function generatePdf(html) {
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
try {
// waitUntil networkidle0 waits for fonts and async content to load
await page.setContent(html, { waitUntil: 'networkidle0' });
const pdf = await page.pdf({
format: 'A4',
printBackground: true,
margin: { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' },
});
return pdf; // Buffer
} finally {
await browser.close();
}
}In production, do not launch a fresh browser per request. Launching Chromium costs a few hundred milliseconds, so reuse one browser instance and open and close only the page. That pattern, plus its trade-offs, is covered in Playwright vs Puppeteer for PDF generation.
page.pdf() accepts the print parameters you expect: format, width, height, margin, scale, landscape, printBackground, displayHeaderFooter, headerTemplate, footerTemplate, and pageRanges.
How do you generate a PDF with wkhtmltopdf?
wkhtmltopdf is a CLI: you pass it an input (a URL or an HTML file) and an output path. From Node.js you either shell out to that binary or use a community wrapper that does the same.
# Basic CLI usage: input then output
wkhtmltopdf input.html output.pdf
# With page size, margins, and background printing
wkhtmltopdf \
--page-size A4 \
--margin-top 20mm --margin-bottom 20mm \
--margin-left 15mm --margin-right 15mm \
--print-media-type \
--background \
input.html output.pdfFrom Node.js, you spawn the binary yourself:
const { execFile } = require('node:child_process');
const fs = require('node:fs/promises');
const os = require('node:os');
const path = require('node:path');
async function generatePdf(html) {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'wk-'));
const inFile = path.join(dir, 'in.html');
const outFile = path.join(dir, 'out.pdf');
await fs.writeFile(inFile, html);
await new Promise((resolve, reject) => {
execFile(
'wkhtmltopdf',
['--page-size', 'A4', '--background', inFile, outFile],
(err) => (err ? reject(err) : resolve()),
);
});
const pdf = await fs.readFile(outFile);
await fs.rm(dir, { recursive: true, force: true });
return pdf; // Buffer
}The wkhtmltopdf binary must already be installed on the machine or in the container image. There is no npm package that bundles a maintained binary, because the binary itself is no longer maintained.
How do you migrate from wkhtmltopdf to Puppeteer?
Replace the CLI call (or wrapper) with puppeteer.launch(), load your HTML, and call page.pdf(). Then map your old flags to page.pdf() options and re-test the CSS, because Chromium renders modern features the old engine silently dropped.
The flag mapping is direct:
| wkhtmltopdf flag | Puppeteer page.pdf() option |
|---|---|
--page-size A4 | format: 'A4' |
--orientation Landscape | landscape: true |
--margin-top 20mm (and friends) | margin: { top: '20mm', ... } |
--background | printBackground: true |
--zoom 1.2 | scale: 1.2 |
--header-html, --footer-html | displayHeaderFooter, headerTemplate, footerTemplate |
--print-media-type | Chromium uses print media by default in page.pdf() |
Two things to check after the switch:
- Re-test the layout. A template tuned for the old WebKit engine may have shims (table-based layout, vendor prefixes) that you can now drop. Conversely, a Grid layout that was broken under wkhtmltopdf will start working, which can shift spacing.
- Re-test fonts. Chromium loads web fonts; use
waitUntil: 'networkidle0'so they finish downloading before the print call.
If you do not want to host Chromium at all, you can also point your existing HTML at a hosted Chromium renderer. Test that path quickly with the free HTML to PDF converterTry it free before writing any code.
Deployment: the real cost of switching
wkhtmltopdf wins on footprint and nothing else. It is a single static binary of about 50 MB with no runtime dependency, which made it easy to drop into a container. Puppeteer ships roughly 300 MB of Chromium plus system libraries (fonts, libX11, and others) and needs a Node.js runtime.
| Concern | Puppeteer | wkhtmltopdf |
|---|---|---|
| Install size | ~300 MB (Chromium) | ~50 MB (binary) |
| Runtime | Node.js | None |
| Docker image growth | 700 MB to 1 GB | Small |
| Serverless fit | Hard (binary over the size limit) | Easier (small binary) |
| Concurrency | One browser, many pages, pooling needed | One process per render |
| Security updates | Yes, via Chromium releases | None |
So the honest trade is: wkhtmltopdf is lighter and simpler to ship, but it is a dead engine with no security patches and no modern rendering. Puppeteer is heavier and needs a Node runtime, but it renders the modern web correctly and is actively maintained. For nearly every team, correct rendering and security updates outweigh the deployment weight.
For detailed render-time and file-size numbers across Chromium-based and CSS-only engines, see the HTML to PDF benchmark 2026.
Is there a maintained drop-in replacement for wkhtmltopdf?
There is no maintained static binary that matches wkhtmltopdf one-for-one while also rendering the modern web. The realistic replacements are all Chromium-based, and you pick based on how much infrastructure you want to own.
- Puppeteer or Playwright (Node.js). Most control, you host Chromium yourself. Playwright adds Firefox and WebKit and ships first-party TypeScript types; the two are very close for PDF work, as covered in Playwright vs Puppeteer.
- A self-hosted HTTP service. Gotenberg wraps Chromium behind an HTTP API in a Docker container, so your app sends HTML and gets a PDF back without embedding a browser in your own service.
- A managed PDF API. PDF4.dev runs headless Chromium server-side and exposes it over an HTTP endpoint, so you ship no Chromium, no binary, and no pooling code. The same engine that Puppeteer drives renders your HTML, and you get the modern CSS and JavaScript support that wkhtmltopdf lacks.
A managed call looks like this:
const res = await fetch('https://pdf4.dev/api/v1/render', {
method: 'POST',
headers: {
Authorization: 'Bearer p4_live_xxx',
'Content-Type': 'application/json',
},
body: JSON.stringify({ html, format: { preset: 'a4' } }),
});
const pdf = await res.arrayBuffer();If you are migrating off wkhtmltopdf and do not want to manage a Chromium install, PDF4.dev runs the same headless Chromium engine as Puppeteer behind an HTTP endpoint, so you skip the 300 MB binary, the Docker bloat, and the concurrency pooling. The output matches what Chrome renders.
Which should you pick?
For any new project in 2026, pick a Chromium-based path. Puppeteer is the default if you want to host the browser yourself in Node.js: it is maintained, it renders modern CSS and JavaScript, and it gets security updates through Chromium.
Keep wkhtmltopdf only when you have a legacy system that you cannot change yet and whose templates are plain CSS 2.1 with no JavaScript. Even then, treat it as technical debt to retire, because it will never receive another security patch.
If the deployment weight of Chromium is the blocker, the answer is not to stay on a dead engine: it is to move the browser off your servers. A self-hosted service like Gotenberg or a managed API like PDF4.dev gives you correct Chromium rendering without shipping the binary yourself.
For the deeper Node.js comparison, see Playwright vs Puppeteer for PDF generation. For the Python side, see Playwright vs WeasyPrint. For render-time and file-size data, see the HTML to PDF benchmark 2026. To test a render in the browser without writing code, use the free HTML to PDF tool.
Free tools mentioned:
Start generating PDFs
Build PDF templates with a visual editor. Render them via API from any language in ~300ms.


