Serverless functions / Vercel
Puppeteer on Vercel: a bundler that cannot see the browser
The build succeeds. The function either exceeds the size limit at deploy time, or deploys and then fails on the first request because the browser executable is not where the library expects it.
What is actually wrong
Two failures wearing the same coat. A plain puppeteer install brings a Chromium that pushes the function past the size cap. Working around that by fetching the browser at runtime hits the second problem: the build traces which files each function needs by following imports, and a file fetched over the network at request time is not an import, so nothing reserves space for it and nothing verifies it exists.
What Vercel gives you to work with
serverless functions built from your repository, plus an edge runtime that is a different environment entirely.
- Packaging: there is a cap on the deployed size of a function, and a full browser install is comfortably past it. The bundler also traces which files a function needs, so a browser that is fetched rather than imported is invisible to it and gets left out of the bundle.
- Filesystem: read-only except /tmp. On the edge runtime there is no filesystem at all, and no child_process either, so nothing that spawns a binary can run there under any configuration.
- Processes: a Node function can spawn a process for the duration of the request. There is a maximum execution duration and it is short by the standards of a cold browser launch.
The fix, if you are keeping Puppeteer
Use puppeteer-core with a Chromium build packaged for this exact situation, where the browser is not in the bundle at all but is downloaded from a URL on the first cold start and cached in /tmp. The bundle stays small because the browser genuinely is not in it.
import chromium from "@sparticuz/chromium-min";
import puppeteer from "puppeteer-core";
// The browser is fetched from this URL on a cold start, not bundled.
// The version in the URL must match the puppeteer-core version.
const executablePath = await chromium.executablePath(
"https://your-storage/chromium-pack.tar",
);
const browser = await puppeteer.launch({ args: chromium.args, executablePath });What still hurts after that
Your PDF endpoint now depends on a file download completing before it can answer a request, which means a cold start includes an unbounded network fetch and an outage in whatever hosts that tarball is an outage in your invoicing. None of this is available on the edge runtime, which has no filesystem and no child_process, so the two runtimes in the same project cannot both render.
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 Vercel 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 Vercel at all?
Use puppeteer-core with a Chromium build packaged for this exact situation, where the browser is not in the bundle at all but is downloaded from a URL on the first cold start and cached in /tmp. The bundle stays small because the browser genuinely is not in it. 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 Vercel 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.
Playwright on Vercel: an install step the build never runs
Deploy succeeds, first request fails with "Executable doesn't exist at ...ms-playwright/chromium-...".
Puppeteer on AWS Lambda: the 250 MB package limit
The deploy fails before the function ever runs, with an error naming the unzipped size of the code and layers.
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.
Puppeteer on Google Cloud Run: 64 MB of shared memory
Renders succeed in testing and fail under load with "Target closed", "Protocol error (Page.navigate): Session closed" or a tab that simply disappears.
Puppeteer on Azure Functions: a plan that installs nothing
On the Consumption plan the browser either fails to start with a missing shared library, or is blocked from spawning at all.
Every renderer, on every platform
The full grid, grouped by what kind of environment the platform is.
What goes wrong when HTML becomes a PDF
Once it deploys, the next set of problems is in the output rather than the infrastructure.
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.