Platform as a service / Heroku
Puppeteer on Heroku: the 500 MB slug and the R14 that follows
Either the push is rejected because the compiled slug is too large, or it deploys and the logs fill with memory quota warnings while requests time out.
What is actually wrong
A compiled slug is capped at 500 MB and a bundled Chromium is a large fraction of that before the application is counted. If it does fit, the second limit arrives: the browser's memory is charged to the dyno, the smaller dyno sizes have well under a gigabyte, and exceeding the allowance produces a memory quota warning and swapping rather than a crash. The application keeps running and gets slower, which is harder to diagnose than an outright failure.
What Heroku gives you to work with
a slug built by buildpacks from your repository, run on a dyno with an ephemeral filesystem.
- Packaging: the compiled slug has a 500 MB limit. A browser binary plus its shared libraries is a large fraction of that before your application is counted.
- Filesystem: writable but ephemeral, and reset on every dyno restart, which happens at least daily. Nothing written at runtime survives.
- Processes: a child process can be spawned, and it competes for the dyno's memory allowance. Exceeding that allowance produces a memory quota error in the logs rather than a crash, and the dyno keeps running while swapping.
The fix, if you are keeping Puppeteer
Add the community buildpack that installs Chromium and its shared libraries outside the application's own dependencies, so the slug carries the browser once rather than inside node_modules. Then size the dyno for a browser rather than for a web server.
# The browser comes from a buildpack, ahead of the language buildpack.
heroku buildpacks:add --index 1 https://github.com/jontewks/puppeteer-heroku-buildpack
# And the browser needs the sandbox flag, because the dyno is
# already a restricted environment.
# puppeteer.launch({ args: ["--no-sandbox", "--disable-dev-shm-usage"] })What still hurts after that
The buildpack is community maintained, so a platform stack upgrade can break the build with no fix available until someone volunteers one. The filesystem is ephemeral and reset at least daily, so nothing can be cached between restarts. And a dyno sized for a browser is idle most of the time, which means paying continuously for capacity that is used in bursts.
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 Heroku 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 Heroku at all?
Add the community buildpack that installs Chromium and its shared libraries outside the application's own dependencies, so the slug carries the browser once rather than inside node_modules. Then size the dyno for a browser rather than for a web server. 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 Heroku 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.
wkhtmltopdf on Heroku: a binary with nowhere to live
Locally it works because the tool is installed on your machine. On a dyno, the command is not found.
WeasyPrint on Heroku: an Aptfile and a library path
Deployment succeeds and the app crashes on boot with "cannot load library 'libpango-1.0-0'".
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 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.
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.
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.