View all our guides

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.

What stays on your side with each kind of Puppeteer alternativeThree boxes from top to bottom. A renderer you run (a browser or a layout engine): you keep engine updates, fonts, memory and restarts, and scaling. A browser someone else runs: you keep your browser code, waits and timeouts. No browser, a PDF API: you keep one HTTP request.A renderer you runEngine updates and fontsMemory, crashes and restartsScaling and queueingA browser someone else runsYour browser codeWaits and timeoutsNo browser: a PDF APIOne HTTP requestWhat stays on your side

Best Puppeteer alternatives for PDF generation in 2026

OptionWhat you still runWhat it costs you
PDFShiftNothing: one POST with your HTML or a URL, the PDF comes back50 free credits a month, no card; $9 a month for 500
Api2PdfNothing: you get back a link to the file, deleted after 24 hours$1 a month plus usage
PDFBoltNothing50 free documents a month; $19 a month for 2,000
DoppioNothing400 free docs a month; $16 a month for 4,000
DocRaptorNothing (runs the Prince engine, not Chromium)5 free documents a month; $15 a month for 125
BrowserlessYour Puppeteer or Playwright code, connected to their browsers, or a call to their /pdf endpoint1,000 free units a month (a unit is up to 30 seconds of browser time)
PlaywrightChromium, on your serversFree and open source, plus your servers
GotenbergA Docker container with Chromium and LibreOfficeFree and open source, plus your servers
WeasyPrintA Python library with its own layout engineFree and open source, plus your servers
wkhtmltopdfAn old Qt WebKit binary, archived since January 2023Free 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:

Three defaults that change the PDF when you switchPuppeteer's default next to PDFShift's for three settings. Paper size: Letter in Puppeteer, A4 in PDFShift. Backgrounds: off in Puppeteer, on in PDFShift. Stylesheet: print in Puppeteer, screen in PDFShift.PuppeteerPDFShiftPaperLetter→A4BackgroundsOff→OnStylesheetPrint→Screen
  • Paper size. Send format: 'Letter' to keep US paper.
  • Backgrounds. They're on already. Send disable_backgrounds: true if you printed without printBackground.
  • Stylesheet. Send use_print: true to render with your print CSS, the way page.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:

PuppeteerPDFShift
page.setContent(html) or page.goto(url)source takes either
landscape: truelandscape: 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 totalPagesheader 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 millisecondstimeout 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.