Skip to content

Playwright vs Puppeteer for PDF generation: which to pick

Both drive Chromium, so the PDF is the same. The real difference is the API shape, the operational tax, and the moment you should stop running browsers on your own servers at all.

If you are choosing between Playwright and Puppeteer to generate PDFs, here is the part nobody leads with: for PDF output specifically, the two produce nearly identical files, because both drive the same Chromium engine and call the same print-to-PDF function. The interesting differences are everywhere else, in the API, the browser management, the language support, and the operational load you sign up for. This post walks the real distinctions for PDF work, then makes the case that for a lot of teams the right answer is neither, at least not on your own servers.

Same engine, same PDF

Puppeteer was built by the Chrome DevTools team to drive Chromium. Playwright was built later by many of the same people at Microsoft, with a broader remit: one API across Chromium, WebKit and Firefox, and first class bindings for JavaScript, Python, .NET and Java. For PDF generation that cross browser reach mostly does not matter, because reliable PDF output is a Chromium feature. WebKit and Firefox headless do not expose the same print-to-PDF capability, so when you generate a PDF with Playwright you are almost always using its Chromium, producing the same bytes Puppeteer would.

So if the question is strictly output fidelity, it is a wash. Real CSS grid, flexbox, web fonts and JavaScript all render correctly in both, because it is the same browser doing the work. Anyone claiming one produces visibly better PDFs than the other for the same HTML is measuring noise.

Where they actually differ

Waiting for content, the thing that breaks most renders

The number one cause of a bad PDF is rendering before the page is ready, and this is where the API ergonomics diverge. Playwright has auto-waiting built into its actions and a rich set of explicit waits, including wait_for_load_state("networkidle") and wait_for_selector. Puppeteer has the same capability through waitUntil: 'networkidle0' and waitForSelector, but its auto-waiting is less pervasive, so you tend to write the waits out more often. In practice both can hold a render until your content lands; Playwright just makes the correct thing slightly more automatic.

// Playwright (Node): wait for the real content, then print
const page = await browser.newPage();
await page.goto('https://app.example.com/invoices/8412',
  { waitUntil: 'networkidle' });
await page.waitForSelector('#invoice-total');
await page.pdf({ path: 'invoice.pdf', format: 'A4',
  printBackground: true });
// Puppeteer (Node): the same job, near-identical API
const page = await browser.newPage();
await page.goto('https://app.example.com/invoices/8412',
  { waitUntil: 'networkidle0' });
await page.waitForSelector('#invoice-total');
await page.pdf({ path: 'invoice.pdf', format: 'A4',
  printBackground: true });

Note printBackground: true in both. Chromium drops background colors and images in print mode by default, and forgetting this flag is the most common reason a PDF comes out looking washed out compared to the screen.

Browser installation and versioning

Puppeteer bundles a known-good Chromium and downloads it on install, which is convenient and occasionally fights your CI cache. Playwright manages browsers through its own CLI (playwright install) and pins versions per release, which is cleaner in Docker and more predictable across a team, at the cost of an extra install step. For a PDF service running one browser, this is a minor difference. For a fleet across many environments, Playwright's explicit version management tends to cause fewer surprises.

Language support

If your stack is not Node, this can decide it outright. Playwright has official, maintained bindings for Python, .NET and Java in addition to JavaScript. Puppeteer is JavaScript first; the community PuppeteerSharp port for .NET and pyppeteer for Python exist but are not first party and lag upstream. A Python or C# team that wants a supported browser automation library will usually land on Playwright for that reason alone.

Playwright vs Puppeteer for PDF, at a glance

Puppeteer Playwright
PDF output quality Chromium print-to-PDF Identical, same Chromium
Official languages JavaScript JS, Python, .NET, Java
Auto-waiting Manual waits, fully capable Built into actions, plus explicit waits
Browser management Bundled Chromium on install Versioned via its own CLI
PDF from WebKit or Firefox No, Chromium only No, Chromium only in practice
Operational load You run the fleet You run the fleet

The tax neither one removes

Look at the last row of that table. Whichever you pick, you are now running a headless browser as part of your infrastructure, and that bill is identical for both. A single Chromium instance idles at 100 to 300 MB and spikes far higher on a heavy page. The processes leak memory and leave zombies under sustained load, so every serious deployment grows a process reaper, a restart-after-N-renders policy, and a concurrency queue to stop the box tipping over. In containers you also carry Chromium's system dependency list and keep it patched. This is real, recurring engineering, and choosing Playwright over Puppeteer does nothing to reduce it, because the weight is in the browser, not the library wrapping it.

Standing up and maintaining the servers that carry a browser fleet is its own discipline, which is why teams running this at scale often lean on managed server provisioning and zero downtime deployment rather than hand rolling it. It is worth being honest that the automation library is the small part of the total cost.

When the answer is neither, on your servers

If PDF generation is core to your product and you have a team that enjoys owning browser infrastructure, run Playwright or Puppeteer and own it well. But if generating PDFs is a supporting feature, invoices after checkout, monthly statements, exported reports, then operating a Chromium fleet to produce them is a lot of undifferentiated work. A rendering API runs the exact same Chromium print path you would, behind an HTTP call, and hands back the file with the fleet as someone else's pager. The full cost breakdown of self hosting versus calling out is in HTML to PDF library vs API, and if you are specifically tired of babysitting headless Chrome, the Puppeteer alternative page makes the direct case.

# The same Chromium render, as one HTTP call
curl https://api.sitepdf.com/v1/render \
  -H "Authorization: Bearer $SITEPDF_KEY" \
  -d url="https://app.example.com/invoices/8412" \
  -d wait_for="#invoice-total" \
  -d format=Letter \
  -d archive=true

The wait_for parameter is the same idea as waitForSelector, holding the render until your content is on screen. The archive=true flag is the part a self hosted browser does not give you at all: it stores a timestamped snapshot of the source page alongside the PDF, so months later you can show what the page said when you generated the document. You can see the render behavior without writing code in the free in-browser converter.

The bottom line

For PDF generation, pick Playwright if your stack is Python, .NET or Java, or if you want the cleaner browser versioning and auto-waiting; pick Puppeteer if you are Node only and want the smallest, most established dependency. The PDF itself will look the same either way, because it is the same engine. The bigger decision is whether you should be running that engine at all: if PDFs are a feature rather than the product, the browser fleet is the expensive part to own, and an API that runs the identical Chromium render is usually the cheaper place to put it.

Written by the team building Sitepdf, an HTML to PDF API that archives every page it renders. The in-browser converter is free to try; early access locks the launch pricing.

§ Early access

Get on the early-access list

The API opens to the list first, in order. Early access locks the planned launch rates for 12 months. No card required, launching soon.

Render + archive, one API