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.