Puppeteer alternatives for PDF generation in 2026
Updated by Cyril Nicodème
People don't buy PDFs. They buy not having to run Chromium. So if Puppeteer only makes your PDFs, pick its replacement by how much of the browser you still want to run. Playwright and Gotenberg keep the browser on your servers, and Browserless runs it for you. A PDF API like PDFShift takes your HTML in one request and sends back the PDF, so there's no browser on your side at all.
Best Puppeteer alternatives for PDF generation in 2026
| Option | What you still run | What it costs you |
|---|---|---|
| PDFShift | Nothing: one POST with your HTML or a URL, the PDF comes back | 50 free credits a month, no card; $9 a month for 500 |
| Api2Pdf | Nothing: you get back a link to the file, deleted after 24 hours | $1 a month plus usage |
| PDFBolt | Nothing | 50 free documents a month; $19 a month for 2,000 |
| Doppio | Nothing | 400 free docs a month; $16 a month for 4,000 |
| DocRaptor | Nothing (runs the Prince engine, not Chromium) | 5 free documents a month; $15 a month for 125 |
| Browserless | Your Puppeteer or Playwright code, connected to their browsers, or a call to their /pdf endpoint | 1,000 free units a month (a unit is up to 30 seconds of browser time) |
| Playwright | Chromium, on your servers | Free and open source, plus your servers |
| Gotenberg | A Docker container with Chromium and LibreOffice | Free and open source, plus your servers |
| WeasyPrint | A Python library with its own layout engine | Free and open source, plus your servers |
| wkhtmltopdf | An old Qt WebKit binary, archived since January 2023 | Free and open source, plus your servers |
What running Puppeteer for PDFs costs you
Puppeteer costs nothing to install. The bill comes from the browser it drives, and it grows when PDFs pile up at once.
We tested Puppeteer 25.16.0 on one invoice, on an Apple M5 Pro. Launching a browser for each PDF took 1.47 seconds a job. Reusing one browser and waiting for load plus document.fonts.ready got it to 0.24 seconds, but now you have to manage that browser. Leave pages open after each job, and Chromium goes from 323 MB to 5,046 MB over 200 jobs. We walk through the fixes in Puppeteer PDF in production.
Puppeteer vs Playwright vs Gotenberg vs WeasyPrint for PDFs
Playwright is the closest swap. It drives the same Chromium with the same page.pdf() call, and it's available for Node, Python, Java and .NET. It doesn't take the browser off your hands, though. In our test, 200 browser contexts left open grew the browser to 7,734 MB, the same memory leak under a new name. Our Playwright PDF guide covers the timeouts and cleanup it needs on a server.
Gotenberg moves Chromium into a Docker container: you start it with docker run --rm -p 3000:3000 gotenberg/gotenberg:9 and send files to it as multipart form data. Your app no longer has browser code in it, but the container is still a server you run, size and restart.
WeasyPrint isn't a browser. It's a Python library with its own CSS layout engine, so HTML built and tested in Chrome can lay out differently.
wkhtmltopdf: don't start a new project on it. Its repo is read-only, so nothing in it gets fixed.
Switching from Puppeteer to a hosted HTML to PDF API
We've run Chromium for PDFShift customers since 2018. Here's the Puppeteer code you get to delete:
const page = await browser.newPage();
try {
await page.setContent(html, { waitUntil: 'load' });
return await page.pdf({
format: 'A4',
printBackground: true,
margin: { top: '40px', right: '40px', bottom: '40px', left: '40px' },
});
} finally {
await page.close();
}
And the same document from PDFShift's conversion API:
const response = await fetch('https://api.pdfshift.io/v3/convert/pdf', {
method: 'POST',
headers: {
'X-API-Key': process.env.PDFSHIFT_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
source: html, // raw HTML or a URL
format: 'A4',
margin: '40px', // all four sides
use_print: true, // print CSS, like page.pdf()
}),
});
if (!response.ok) {
throw new Error(`PDFShift returned ${response.status}: ${await response.text()}`);
}
const pdf = Buffer.from(await response.arrayBuffer());
Three defaults differ, and each one changes the PDF if you don't set it:
- Paper size. Send
format: 'Letter'to keep US paper. - Backgrounds. They're on already. Send
disable_backgrounds: trueif you printed withoutprintBackground. - Stylesheet. Send
use_print: trueto render with your print CSS, the waypage.pdf()does.
To see your HTML at A4 width with its print CSS before you send it, paste it into our free HTML viewer.
The rest of the options map one to one:
| Puppeteer | PDFShift |
|---|---|
page.setContent(html) or page.goto(url) | source takes either |
landscape: true | landscape: true |
margin: { top, right, bottom, left } | margin as an object, or a CSS-style string such as '40px 20px' |
pageRanges: '1-5' | pages: '1-5' |
scale (0.1 to 2) | zoom (0.1 to 2) |
headerTemplate and footerTemplate with pageNumber and totalPages | header and footer, each with its own source, and {{ page }}, {{ total }} and {{ date }} |
page.goto(url, { waitUntil: 'networkidle0' }) | wait_for_network, on by default: waits until no request has run for 500 ms |
page.waitForFunction(...) | wait_for: the name of a global function that returns true when the page is ready |
page.addStyleTag(), page.evaluate() | css and javascript |
timeout in milliseconds | timeout in seconds |
PDFShift headers and footers don't inherit your page's CSS, so style them inline. While you compare output, send sandbox: true. Those documents are free and watermarked, up to 10 a minute.
That's the whole migration. Get a free API key and send it the HTML you print with Puppeteer today.
We hope this guide was helpful. If you have any questions or noticed any issues on the code above,
feel free to drop us a line.