SaaS stacks
Aaron Brick9 min read73 views

Gotenberg vs Puppeteer vs react-pdf: Next.js PDF Generation (2026)

Three ways to make a PDF from a Next.js app, compared on the thing the listicles skip: where the Chromium binary lives, and who is responsible for keeping its version aligned. One of the three currently asks you to bridge a three-major-version gap.

Minimalist diagram of three application boundaries, one holding a small document glyph, one strained by an oversized dark circle, and one holding a neatly fitted cylinder
Minimalist diagram of three application boundaries, one holding a small document glyph, one strained by an oversized dark circle, and one holding a neatly fitted cylinder
On this page

Quick answer (2026)

If your PDFs are documents you control end to end, invoices, receipts, statements, contracts, use react-pdf logo react-pdf. It writes PDFs directly with PDFKit and never launches a browser, so there is no Chromium in your deployment bundle and no version to keep aligned. We measured @react-pdf/renderer 4.9.0 at 312 KB unpacked on September 1, 2026.

If your PDFs are existing web pages you already render, and you want the output to match the browser exactly, run Gotenberg logo Gotenberg as a separate service. It is a Go binary wrapping Chromium and LibreOffice behind a REST API, MIT licensed, and the important property is not a feature: the 665.7 MB of image it carries sits outside your application, so it never counts against your function bundle.

Run Puppeteer logo Puppeteer in-process only when you already operate a long-lived Node server with a real filesystem. On serverless it is the option with a maintenance cost the other two do not have, and that cost is currently at its widest point in a year. We measured it below.

The number that decides this, measured September 1, 2026

The standard recipe for running Puppeteer on Vercel is puppeteer-core plus @sparticuz/chromium, because the full puppeteer package downloads a Chromium build that will not survive a serverless deploy. That recipe has a moving part almost no write-up mentions: the two packages are versioned independently, and you own the alignment.

We read both from the npm registry and from Puppeteer's own tagged source on September 1, 2026:

Scroll to see more

PackageLatestPublishedChrome it expects
puppeteer / puppeteer-core25.9.02026-08-25152.0.7977.54
@sparticuz/chromium149.0.02026-05-27149

That is three major Chrome versions and three months apart. @sparticuz/chromium supplies the binary, puppeteer-core supplies the driver that speaks to it, and the DevTools protocol between them is not guaranteed to match across that gap. So installing both at latest today gives you a pairing nobody tested.

This is not a criticism the maintainer would dispute. It is stated plainly in the package's own README:

> maintaining the package became difficult due to the rapid pace of puppeteer updates. @sparticuz/chromium is not tied to specific puppeteer versions

and the README tells you to go and look up the correct pairing yourself on Puppeteer's Chromium support page before installing either one.

There is a second line in that README worth reading twice:

> Because this package follows Chromium's release cycle, it does NOT follow semantic versioning. Breaking changes may occur at the 'patch' level.

A caret range on that dependency is therefore not a safe default. Pin it exactly.

The honest reading, because the obvious one is too harsh

The obvious conclusion from a three month release gap is that the project is drifting. We checked before writing that, and it is not the right reading.

The Sparticuz/chromium repository was last pushed on August 25, 2026, seven days before we measured. It is not archived, it has 22 open issues, and the release cadence is deliberately tied to Chromium's cycle rather than to Puppeteer's. Chromium simply ships faster than one maintainer publishes 66 MB binaries, and the package documents that constraint instead of hiding it.

So the finding is not "this is abandoned". It is narrower and more useful: the serverless Puppeteer path structurally requires you to pin two packages against a third party's compatibility table, and the gap you have to bridge is wider at some moments than others. Right now it is wide. That is a real recurring maintenance cost, and it is the cost the other two options do not charge.

Where the bytes actually live

This is the whole architectural decision, and it is easier to see as a table than as prose. Measured September 1, 2026.

Scroll to see more

react-pdfPuppeteer + sparticuzGotenberg
Rendering enginePDFKit, no browserChromium, in your functionChromium, in a container
Payload you deploy312 KB npm66.4 MB npm, before Puppeteer itself0, it is a separate service
Where the big bytes sitnowhereinside your bundle665.7 MB image, outside your app
Who owns version alignmentnobody, there is noneyouthe Gotenberg maintainers
LicenceMITApache-2.0 and MITMIT
Renders your existing CSSno, its own layout engineyesyes

Vercel documents a 250 MB uncompressed limit for functions, which is the wall the 66.4 MB Chromium payload is pushing against. Two honest qualifications on that number, because it is quoted as an absolute far more often than it deserves. First, Vercel now documents a large functions beta supporting up to 5 GB, so the 250 MB ceiling is no longer the hard stop it was in 2024. Second, and more usefully, the limit is not really the point. Even when the bytes fit, you still own the version pinning above, and that does not get cheaper on a bigger function.

Gotenberg's 665.7 MB is by far the largest number in the table and it is the one that matters least, because those bytes are pulled once by your container host and never travel with a deploy.

Where each one actually loses

react-pdf loses on fidelity, and it is not close. It does not render your web page. It has its own layout engine and its own component primitives, so you write the document a second time. If your requirement is "make this existing page a PDF", react-pdf is the wrong tool and no amount of effort fixes that. It also carries 311 open issues against 16,765 stars, which for a project of that size is a real queue rather than an alarming one.

Puppeteer loses on operations, everywhere except a long-lived server. The version pinning above is the recurring cost. On top of it you are running a browser process inside a request handler, with the cold starts and memory ceilings that implies. @sparticuz/chromium 149.0.0 also declares engines: node ^22.17.0 || >=24.0.0, so it will refuse to install on Node 20. Check your runtime before you plan around it.

Gotenberg loses if you have nowhere to put it. It is a service, so it needs somewhere to run, a network hop, and a health check. If your entire deployment target is serverless functions with no container alongside them, Gotenberg is not available to you and the honest answer is react-pdf or a hosted API, not Gotenberg. Its release cadence is also lumpier than Puppeteer's: v8.36.0 on August 14, v8.35.0 on August 7, then a gap back to v8.34.0 on June 12, 2026.

What we did not measure, and why we are saying so

We did not benchmark render latency or output fidelity. Every latency number in this category is dominated by document complexity and by whether the browser is warm, so a single figure from our machine would be worse than no figure. The GitHub discussion on Gotenberg's own repository about baseline conversion latency is the better place to start, because it is the maintainers arguing about their own overhead.

We also did not test Playwright, which will do the same job as Puppeteer and has the same in-process shape. If you already run Playwright for tests, use it and skip Puppeteer. The architectural question in this article is unchanged by that choice: it is still a browser inside your process.

The search signal, since it points the same way

We pulled US keyword data on September 1, 2026 while scoping this piece, and the direction was consistent enough to include. The service-shaped tools are growing and the in-process libraries are shrinking: gotenberg at 720 searches a month is up 49 percent year on year, weasyprint at 1,600 is up 46 percent, and the phrase html to pdf api is up 56 percent. Over the same window react pdf fell 70 percent and playwright pdf fell 22 percent.

Read that carefully, because the tempting conclusion is wrong. @react-pdf/renderer pulled 20.9 million npm downloads in the last month and shipped five releases in the two weeks to August 27, 2026. It is not in decline as software. What is declining is people searching for it, which is what usually happens when a library stops being novel and becomes furniture. Treat the trend as evidence about where new attention is going, not as evidence about what is healthy.

If you are choosing today, the order that works

  1. Ask whether the PDF is a document or a page. A document you design once goes to react-pdf and stops being an infrastructure problem.
  2. If it is a page, ask whether you can run a container. If yes, Gotenberg, and the decision is finished.
  3. If it is a page and you are serverless only, use Puppeteer with both versions pinned exactly, read the Chromium support table first, and put a note in your README saying why the versions look old. The next person to run npm update will otherwise break it.
  4. Decide where the output goes before you build the generator. A PDF pipeline that has nowhere durable to write is a worse problem than the renderer choice, and it is the part people leave until last. We covered the storage options in our file upload comparison and the self-hosted end in the object storage roundup.

Sources we read directly

Every number above came from a primary source on September 1, 2026, not from a secondary article:

  • the npm registry, for versions, publish dates, unpacked sizes, licences and download counts
  • Puppeteer's revisions.ts at the puppeteer-v25.9.0 tag, for the exact Chrome build it pins
  • the @sparticuz/chromium README, for the versioning policy and the semver warning, quoted above
  • the GitHub API, for release dates, push dates, licences and issue counts
  • the Docker Hub registry API, for the compressed layer total of gotenberg/gotenberg:8 on linux/amd64
  • Vercel's function limits documentation, for the 250 MB figure and the large functions beta

If any of these move, the table moves with them. The architectural point underneath does not: the question is only ever whether the browser lives nowhere, inside your process, or beside it.

Aaron Brick

Written by

Aaron Brick

Aaron Brick curates the ShipGarden gallery, where we test open-source building blocks so we can own the stack that funds the life.

Frequently asked questions

Which Chromium version does @sparticuz/chromium need to match in 2026?

It needs to match whatever Chrome your puppeteer-core version expects, and you have to look that pairing up yourself. Measured on September 1, 2026, puppeteer-core 25.9.0 pins Chrome 152.0.7977.54 while the latest @sparticuz/chromium is 149.0.0, published May 27, 2026. The package README points you at Puppeteer's Chromium support page for the correct pairing, and it warns that because it follows Chromium's release cycle it does not follow semantic versioning, so breaking changes can land at the patch level. Pin both versions exactly rather than using a caret range.

Does react-pdf use Chromium under the hood?

No. Its dependency tree is PDFKit plus its own layout and render packages, with no browser anywhere in it. That is why @react-pdf/renderer 4.9.0 unpacks to about 312 KB while @sparticuz/chromium 149.0.0 unpacks to about 66.4 MB. The trade is fidelity: react-pdf will not render your existing web page, because it has its own layout engine and component primitives, so you author the document a second time.

Can I run Gotenberg on Vercel?

Not inside a Vercel function, because Gotenberg is a service rather than a library. You run its container somewhere that hosts containers and call it over HTTP from your Next.js app. That is precisely why its 665.7 MB image size does not matter: those bytes are pulled once by the container host and never travel with your deploy. If your entire deployment target is serverless functions with nothing running alongside them, the honest answer is react-pdf or a hosted conversion API, not Gotenberg.

Is the Vercel 250 MB function limit the real reason to avoid Puppeteer?

It is the most cited reason and it is the weaker one. Vercel does document a 250 MB uncompressed size limit, but it also documents a large functions beta supporting up to 5 GB, so the ceiling is no longer the hard stop it once was. The more durable objection is the version alignment: even on a bigger function you still own the pinning between puppeteer-core and @sparticuz/chromium, and that maintenance cost does not shrink when the size limit rises.

Is @sparticuz/chromium abandoned given it has not released since May 2026?

No, and the harsher reading does not survive checking. The repository was last pushed on August 25, 2026 and it is not archived. The package deliberately tracks Chromium's release cycle rather than Puppeteer's, and Chromium ships faster than one maintainer publishes 66 MB binaries. The accurate finding is narrower: the serverless Puppeteer path structurally requires you to pin against a third party's compatibility table, and the gap you have to bridge is wider at some moments than others.

Should I use Playwright instead of Puppeteer for PDFs?

If you already run Playwright for tests then yes, use it and skip Puppeteer, since it does the same job. It does not change the architecture question in this comparison though. Playwright is still a browser running inside your process, so it carries the same in-process shape, the same cold starts and the same bundle pressure. The three-way choice is really about whether the browser lives nowhere, inside your process, or beside it.