Guides · 2026-10-17

HTML to PDF in Node.js and Python: 4 approaches compared

Invoices, receipts, reports, tickets, certificates — sooner or later every app needs to turn HTML into a PDF. There are four realistic ways to do it. Here's how each works, with code, and when to pick it.

1. wkhtmltopdf — don't start new projects with it

For years wkhtmltopdf was the default answer. It's built on an old Qt WebKit engine, and the project has since been archived. Practically, that means no flexbox, no CSS grid, spotty modern CSS support, and no more security fixes. Wrappers like pdfkit (Python) and wkhtmltopdf (npm) inherit all of this. If you have a legacy template that works, fine; otherwise move on.

2. WeasyPrint (Python) — great for print-first documents

WeasyPrint is a pure-Python layout engine purpose-built for paged media. It has excellent support for CSS Paged Media (running headers and footers, page counters, @page rules), which browsers implement only partially.

from weasyprint import HTML

HTML(string="<h1>Invoice #42</h1><p>Total: $20</p>").write_pdf("invoice.pdf")

Pick it when: you write the HTML specifically for print (invoices, books, reports) and want "Page 3 of 7" footers. Avoid it when: you need to render existing web pages or anything that relies on JavaScript — WeasyPrint doesn't run JS, and its CSS support, while good, isn't a browser's.

3. Headless Chrome (Puppeteer / Playwright) — what you see is what you get

Chrome's own print engine gives you exactly what Chrome's print preview shows, including JavaScript-rendered content, charts, web fonts, flexbox and grid.

Node.js with Puppeteer:

import puppeteer from "puppeteer";

const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setContent(html, { waitUntil: "networkidle0" });
const pdf = await page.pdf({ format: "A4", printBackground: true });
await browser.close();

Python with Playwright:

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.set_content(html, wait_until="networkidle")
    page.pdf(path="out.pdf", format="A4", print_background=True)
    browser.close()

The catch is operations. Each Chromium instance takes a few hundred MB of RAM, cold starts take seconds, crashed or leaked browser processes need reaping, serverless platforms need special slim builds, and you'll need fonts installed on the host. If you render HTML that includes user-supplied URLs or images, you also need SSRF protection.

4. A hosted API — headless Chrome without the operations

A rendering API runs the browser fleet for you; you POST HTML and get a PDF back. With Shotlane:

// Node.js 18+
const res = await fetch("https://shotlane.dev/v1/pdf", {
  method: "POST",
  headers: { "X-API-Key": process.env.SHOTLANE_KEY, "Content-Type": "application/json" },
  body: JSON.stringify({ html, paper: "A4" }),
});
await fs.promises.writeFile("invoice.pdf", Buffer.from(await res.arrayBuffer()));
# Python
import requests
r = requests.post("https://shotlane.dev/v1/pdf", headers={"X-API-Key": KEY},
                  json={"html": html, "paper": "Letter", "landscape": False})
open("invoice.pdf", "wb").write(r.content)

Pick it when: PDF generation is a feature, not your product, and you'd rather not run Chrome on your servers or in your Lambda.

CSS that makes any of these look better

@page { size: A4; margin: 18mm 16mm; }          /* page size + margins */
h2 { break-after: avoid; }                       /* keep headings with their content */
tr, .card { break-inside: avoid; }               /* don't split rows across pages */
.page-break { break-before: page; }              /* manual page breaks */
thead { display: table-header-group; }           /* repeat table headers on every page */
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; } /* keep backgrounds */

Load web fonts with a normal <link> or @import and wait for them before printing (networkidle does this); they're embedded into the PDF.

Quick decision table

JavaScriptModern CSSPaged-media featuresOps burden
wkhtmltopdfOld enginePoorSomeLow, but unmaintained
WeasyPrintNoGoodExcellentLow
Puppeteer / PlaywrightYesExcellentPartialHigh
Hosted APIYesExcellentPartialNone

Want to see how your template renders in Chromium right now? Paste it into the free HTML to PDF converter.

Skip the browser babysitting

Shotlane runs hardened headless Chromium for you: screenshots and PDFs from one HTTP request, with cookie-banner hiding, ad blocking, signed embed URLs and free cache hits. Prepaid credits that never expire.

100 free renders · no credit card · key shown instantly