PDFPipe

Serverless functions / Netlify Functions

Puppeteer on Netlify Functions: esbuild does not copy binaries

The function deploys and then fails at runtime saying the Chromium revision could not be found.

What is actually wrong

Functions are bundled with esbuild, which builds a dependency graph by following imports and includes what that graph reaches. A browser binary sitting in node_modules is reached by nothing: puppeteer resolves it by filesystem path at runtime. The bundler is behaving correctly and the result is a function with a library and no browser.

What Netlify Functions gives you to work with

AWS Lambda underneath, with Netlify's own bundling step in front of it.

  • Packaging: the same Lambda limits apply, but the bundler is the part that bites first. Functions are bundled with esbuild by default, which follows imports and copies what it finds. A browser binary is not an import, so it is silently omitted and the function fails at runtime rather than at build time.
  • Filesystem: read-only except /tmp, inherited from Lambda.
  • Processes: a child process runs for the invocation. Background functions get a longer window than synchronous ones, which is the only real lever available.

The fix, if you are keeping Puppeteer

Mark the package as external so it is copied rather than traced, or switch that function to the bundler that copies node_modules wholesale. Both work, and both then land on the underlying Lambda size limit, because Netlify Functions are Lambda functions and inherit its 250 MB.

toml
# netlify.toml
[functions]
  # Copy rather than trace. Fixes the missing binary, does not fix size.
  external_node_modules = ["puppeteer-core", "@sparticuz/chromium"]
  node_bundler = "esbuild"

What still hurts after that

Marking things external is a per-package list that has to be maintained by hand, and forgetting an entry produces the same runtime-only failure again. Once the binary is included you are back to the Lambda package limit, so the compressed-Chromium approach is needed here too, with the extra step of persuading the bundler not to touch it.

What Puppeteer is genuinely good at

it renders exactly what a browser renders, which means modern CSS, web fonts and client-side JavaScript all behave the way they do in a tab. That is worth keeping in view: the problem on this page is where it runs, not what it produces. Puppeteer is a Node library that drives a real Chromium, and downloads that Chromium into node_modules when it is installed. It needs the browser binary, a long list of shared libraries the browser links against, fonts, and enough memory for a browser process that is not accounted for by your runtime's heap settings.

The other shape of the answer

Every fix above keeps the renderer inside your deployment, which means the constraint stays yours: the package size, the shared libraries, the fonts, the memory, the patching. The alternative is an HTTP call from Netlify Functions to something that already has a browser in it, which turns all of the above into a request and a response. That is what this API is, and it is the reason these pages exist. Whether that trade is right depends on whether rendering is a thing you want to operate.

Frequently asked

Can Puppeteer run on Netlify Functions at all?

Mark the package as external so it is copied rather than traced, or switch that function to the bundler that copies node_modules wholesale. Both work, and both then land on the underlying Lambda size limit, because Netlify Functions are Lambda functions and inherit its 250 MB. Whether that is worth doing is a separate question from whether it is possible, and the section above on what still hurts is the honest answer to it.

Why does it work locally?

Because a development machine has root, a package manager, several gigabytes of memory, a full font set and no packaging limit. Every one of those is a thing Netlify Functions constrains. A renderer that works locally and fails on deploy has not regressed: it has met the first environment that does not give it everything.

Related failures

The same platform with a different renderer, and the same renderer on other platforms.

If the render moves off this platform, none of the above is your problem to keep solving. 100 free documents a month, and a playground that needs no signup.