It will not run
Chrome processes accumulate and never exit
Process count climbs over hours or days. Memory follows it. A restart fixes everything for a while, which is how the problem gets classified as a mystery rather than a leak.
What is actually happening
A browser launched per request has to be closed, and the close is skipped whenever the render throws before reaching it. Every error path that does not close the browser leaks a process, so the leak rate is proportional to your error rate rather than to your traffic.
Confirming it is this and not something that looks like it
Count browser processes over time against your request count. If the count only ever grows, every one that is still there is one that errored.
The fix
Close in a finally block, not after the work. Then bound the count so a leak degrades rather than exhausts. The structural answer is to not have browser processes in your application at all, since a leaked HTTP connection is a much smaller problem than a leaked browser.
let browser;
try {
browser = await puppeteer.launch(launchOptions);
const page = await browser.newPage();
await page.setContent(html, { waitUntil: "networkidle0" });
return await page.pdf({ format: "A4", printBackground: true });
} finally {
// Not after the return. Not in the happy path. Here.
await browser?.close();
}If that was not it
These produce the same symptom often enough to be worth ruling out before assuming the fix above did not work.
- A timeout that abandons the render without closing the browser it started
- Pages left open inside a shared browser, which leak renderer processes rather than whole browsers
- A container without an init process, so orphaned children are never reaped
- Crashes during close, which leave the process behind despite the call
Frequently asked
Does this happen with every rendering engine?
The behaviour behind it is not specific to one tool. A browser launched per request has to be closed, and the close is skipped whenever the render throws before reaching it. Anything rendering HTML to a paged medium has to make the same decision, so the fix travels with you if you change how the render happens.
Will this show up as an error in my logs?
Yes, this one surfaces as a thrown error or a failed status, which is why it is at least findable. The harder half is that the message often names the call that was in flight rather than the thing that actually failed.
Related failures
Problems people arrive at from the same starting point, or mistake for this one.
Failed to launch the browser process
A headless browser is not a self-contained binary.
libnss3.so: cannot open shared object file
This is the previous error with the first missing library named.
Navigation timeout of 30000 ms exceeded
The default wait condition is the network going idle, and something on the page is keeping it busy: an analytics beacon, a font request to a host that is not responding, a polling request, or an ad script.
Protocol error (Page.printToPDF): Target closed
The tab crashed, and by far the most common reason is memory.
Chromium does not fit in an AWS Lambda deployment package
A full browser build is several hundred megabytes and the unzipped package limit is 250MB.
CSS for paged documents, and what actually works
Which parts of the paged media specification a browser-based render implements.
Everything that goes wrong, by category
The full list, grouped by where in the pipeline it breaks.
Paste your markup and see the rendered document, without signing up.