If you've ever had to generate a PDF from HTML in a Node.js app โ an invoice, a report, a contract โ you've probably hit the same fork in the road. Do you spin up Puppeteer, reach for the old reliable wkhtmltopdf, or pay someone else to run it?
This post walks through the trade-offs honestly. We build HTML-to-PDF.io, so we have a side, but the goal here is to help you pick what actually fits your project โ sometimes that's us, sometimes it isn't.
The three options at a glanceโ
| Puppeteer | wkhtmltopdf | API service | |
|---|---|---|---|
| Rendering engine | Headless Chromium | WebKit (old) | Headless Chromium |
| Setup time | Hours to days | Hours | Minutes |
| Bundle size | ~170MB Chromium binary | ~40MB binary | None (HTTP) |
| Modern CSS support | Excellent (Chrome) | Limited (flexbox quirks, no CSS grid) | Excellent (Chrome) |
| Runs on Lambda / Vercel | Painful (layer hacks) | Painful (binary hacks) | Yes (it's HTTP) |
| Cold start | 2โ5s | 1โ2s | None on your side |
| Memory footprint | 512MB โ 2GB | 256MB โ 1GB | None on your side |
| Cost model | Compute + ops time | Compute + ops time | Per request |
| Maintenance burden | High | Low (but stale) | None |
Option 1: Puppeteerโ
Puppeteer drives a real Chromium instance, so whatever you can render in Chrome, you can render to PDF. That includes modern CSS, web fonts, custom JavaScript, charts โ the works.
import puppeteer from "puppeteer"
const browser = await puppeteer.launch()
const page = await browser.newPage()
await page.setContent("<html><body><h1>Hello World</h1></body></html>")
const pdf = await page.pdf({ format: "A4" })
await browser.close()
It looks simple. It isn't.
Where Puppeteer hurts:
- Chromium is heavy. The download is ~170MB, the running process eats 300MB+ of RAM, and you need to keep it patched.
- Serverless is hostile to it. AWS Lambda's 250MB unzipped layer limit means you need
@sparticuz/chromiumor similar, and even then cold starts are 2โ5 seconds. - Concurrency is painful. A browser instance per request is expensive; a pool of reusable pages is fiddly and leaks memory if you get it wrong.
- Ops burden. Chromium ships security patches monthly. If you're running it in production, you own that update cadence.
Pick Puppeteer if: you already have a long-running Node process (not serverless), you generate PDFs in high enough volume that a managed service costs more than your engineer time, and you have someone willing to own the Chromium upgrade treadmill.
Option 2: wkhtmltopdfโ
The old reliable. wkhtmltopdf is a binary that wraps an old WebKit build to render HTML to PDF. It's been the default answer for over a decade.
import { exec } from "child_process"
exec("wkhtmltopdf input.html output.pdf", (err) => { /* ... */ })
It's smaller and faster to start than Puppeteer, and there's a Node wrapper (wkhtmltopdf on npm). But it carries a heavy caveat.
Where wkhtmltopdf hurts:
- It's effectively unmaintained. The project archived its repository in 2023. No security patches, no bug fixes, no modern CSS additions.
- Old WebKit means broken CSS. Flexbox is partial. CSS grid doesn't exist. Modern fonts and
@media printrules behave inconsistently. If your HTML uses anything from the last five years of CSS, expect surprises. - Still binary-heavy. You can't just
npm installit โ you need the right binary for your OS/architecture, which complicates Docker images and CI.
Pick wkhtmltopdf if: you have legacy HTML that already renders correctly under it and you don't want to migrate. For new projects, the unmaintained status is hard to justify.
Option 3: A managed HTML to PDF APIโ
The third option is to skip running a renderer yourself and hit an API. Send HTML, get back a PDF.
import { htmlToPdf } from "@html-to-pdf.io/sdk-javascript"
const { pdfData } = await htmlToPdf({
apiKey: process.env.HTML_TO_PDF_API_KEY,
html: "<html><body><h1>Hello World</h1></body></html>",
})
That's it. No Chromium binary in your bundle, no Lambda layer hacks, no upgrade treadmill. Modern CSS support because it's still Chrome under the hood โ just running on someone else's infrastructure.
Where it hurts:
- You're paying per request. Above a certain volume, running your own Puppeteer is cheaper on raw compute.
- You're outsourcing reliability. The provider's downtime is your downtime. (We publish ours at status.html-to-pdf.io.)
- Network latency. An HTTP round-trip adds ~50โ200ms vs. local rendering โ usually invisible to users, but worth knowing.
Pick a managed API if: you're on serverless (Lambda, Vercel, Cloudflare Workers), your PDF volume is moderate, or your team would rather ship features than maintain a Chromium fork.
The honest answerโ
For most teams shipping new products in 2026, the maths has shifted. Engineer hours cost more than they used to, serverless platforms have become the default, and the operational cost of running Chromium in production is real even when the compute is "free."
If you're rendering hundreds of thousands of PDFs a day, self-hosting Puppeteer makes sense โ at that scale you have a dedicated team to own it. If you're rendering ten or ten thousand a day, paying a few cents per document to skip the entire problem is a better trade.
Try itโ
If you want to see what the output looks like before committing, we run a free converter at html-to-pdf.io/convert โ paste HTML, get a PDF. No signup.
For the API itself, the docs are here, and pricing starts well below what most teams expect.
