Skip to content
All posts
Engineering· 6 min read · RenderKit team

Why Puppeteer eats your RAM (and what we do instead)

The first version of every screenshot service looks the same: a small Node script, puppeteer.launch(), a queue, and a cheap VM. It works for a week.

The slow leak

Chromium is not designed to be spawned thousands of times in a long-lived process. Pages that never fully close, fonts that stay resident, zombie renderers after a crash — memory creeps up until the OOM killer takes the box. So you add a cron to restart it, then a healthcheck, then a second box for when the first is restarting.

Now you own a browser fleet. That is the job you didn't want.

What actually scales

Pool warm browser contexts, cap concurrency per worker, recycle contexts on a fixed budget, and isolate the navigation timeout from the capture timeout. Put a reservation-before-render credit check in front so a burst can't stampede the pool. Cache by content hash so identical renders never touch a browser at all.

The cheapest render is the one you don't run. Content-addressed caching turns repeat work into an ~80ms CDN hit.

That's the architecture behind /v1/screenshot. You send one POST; we keep the fleet alive. See the Rendering model for the full flow.