Serverless functions / AWS Lambda
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.
What is actually wrong
Installing puppeteer downloads a full Chromium build into node_modules. Uncompressed, that build alone is larger than the 250 MB Lambda allows for the function code and all of its layers combined. Nothing in the application is at fault; the dependency does not fit in the box.
What AWS Lambda gives you to work with
a function that is handed a request, runs, and is frozen again. The execution environment is reused between invocations but you do not control when it is torn down.
- Packaging: 250 MB unzipped for the function code and every layer attached to it, combined. That is the hard number most PDF deployments run into. A container image raises it to 10 GB but changes how the function is built and pushed.
- Filesystem: the deployment package is read-only. Only /tmp is writable, and it defaults to 512 MB. Anything the process writes there survives for the life of the execution environment, not the invocation, which is a subtle source of both caching wins and stale-state bugs.
- Processes: a child process can be spawned and it lives while the invocation does. When the invocation returns, the environment is frozen mid-execution rather than terminated, so a process that has not exited is simply suspended and resumed on the next call.
The fix, if you are keeping Puppeteer
Split the two things puppeteer bundles. Install puppeteer-core, which is the library with no browser, and get the browser from a package built specifically for this limit: a Chromium compiled for the Lambda base image and stored compressed, which is decompressed into /tmp on the first invocation. The compressed build fits inside a layer.
import chromium from "@sparticuz/chromium";
import puppeteer from "puppeteer-core";
// Decompresses Chromium into /tmp on a cold start. Subsequent
// invocations on the same execution environment reuse it.
const browser = await puppeteer.launch({
args: chromium.args,
executablePath: await chromium.executablePath(),
headless: true,
});What still hurts after that
The decompression happens on every cold start and it is not free, so p99 latency now has a step in it that p50 does not show. The Chromium build and the puppeteer-core version have to match, and they are released by different people on different schedules, so a routine dependency bump can produce a protocol error that looks like nothing to do with versions. /tmp defaults to 512 MB, which the decompressed browser eats a large share of before your document does. And the layer has to be rebuilt whenever AWS moves the runtime to a new base image, because the binary was compiled against the old one.
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 AWS Lambda 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 AWS Lambda at all?
Split the two things puppeteer bundles. Install puppeteer-core, which is the library with no browser, and get the browser from a package built specifically for this limit: a Chromium compiled for the Lambda base image and stored compressed, which is decompressed into /tmp on the first invocation. The compressed build fits inside a layer. 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 AWS Lambda 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 AWS Lambda: browsers that are not in your bundle
At runtime, not at deploy time: "Executable doesn't exist at /home/sbx_user1051/.cache/ms-playwright/chromium-.../chrome-linux/chrome".
wkhtmltopdf on AWS Lambda: a binary with no libraries to link against
"error while loading shared libraries: libXrender.so.1: cannot open shared object file", or the process exits with no output and no error at all.
WeasyPrint on AWS Lambda: the libraries pip cannot install
"OSError: cannot load library 'libgobject-2.0-0': libgobject-2.0-0: cannot open shared object file", raised on import rather than on render.
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.
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.