Twenty lines gets you the proof of concept. Puppeteer or Playwright, launch(), goto(), screenshot() — it worked yesterday afternoon and felt great. What follows is about the four hundred lines you haven't written yet, the ones that keep those twenty alive in production. Writing them happens to be our day job, so we have opinions about which ones you can skip. Spoiler: fewer than you'd hope.

The babysitting list

Zombie processes and the memory staircase. Headless Chromium wants 0.5–1 GB per instance and doesn't always hand it back. A crashed render leaves orphaned child processes behind; a hung page takes its whole process tree hostage. Memory climbs the staircase until the OOM killer picks a victim for you — at 3am, usually the wrong one. The fix is a watchdog: process-tree cleanup on timeout, hard memory ceilings, browsers recycled on a schedule. None of it is hard. Every line of it is code you now own.

Fonts and emoji. Your server doesn't know what 日本語 looks like. Out of the box, the container renders CJK text and emoji as tofu — hollow rectangles standing where characters should be — while the screenshot returns 200 OK, since nothing failed except the part someone actually cared about. The cure is fontconfig plus the Noto family plus an emoji font, and a fatter image to ship and patch from then on. A render isn't done when the page loads. It's done when the glyphs exist.

Knowing when a page is actually finished. On any page with an analytics heartbeat or a long-poll connection, networkidle is a polite fiction — it may never fire at all. Lazy-loaded images sit below the fold pretending not to exist. Web fonts arrive late and reflow the exact layout you just captured. Sooner or later, every serious pipeline grows timing heuristics, per-site overrides, and scroll-then-settle logic for full-page captures. It's the least glamorous code you'll ever write, and the most load-bearing.

Concurrency and the queue. A render is a memory spike with a browser attached. Leave requests unbounded and the first traffic bump takes the box down; serialize everything and your p95 goes to lunch. Which means: a job queue, per-worker concurrency caps, retry semantics that can tell "the site was slow" apart from "the URL is garbage", and a timeout budget split three ways across browser, page, and job. Congratulations — you now operate distributed infrastructure. For screenshots.

SSRF. The renderer takes URLs from strangers and opens them inside your network, which makes it an internal port scanner with a screenshot feature. http://169.254.169.254/ — the cloud metadata endpoint — happily hands IAM credentials to anyone asking from the right place. Blocklists alone don't close this; it takes URL normalization, full DNS resolution with every record checked, and a proxy that pins one IP per connection. We documented the whole three-layer guard — closing it properly took serious engineering, and of everything on this list it's the item teams usually discover after the incident.

The upgrade treadmill. Playwright cuts a new minor roughly every month, and Chromium shifts underneath it. An upgrade can move font metrics, color output, or default headers — diffs subtle enough that users report them before you notice. Pin your versions and security debt accrues; track them and you've signed up for a monthly regression pass. A third option does not exist.

The cost math

Run the numbers honestly. Built well, the pipeline above is weeks of engineering — our own first milestone was, more or less, exactly this list — and it keeps charging you after launch: version bumps, alert noise, font bugs, the occasional genuinely strange incident. We do this full time and still get humbled; one config change of ours once left the worker process healthy, the heartbeat green, and precisely nothing rendering. Price the maintenance at a few hours a month, at a loaded engineer rate of $50–100 an hour, and the homegrown service quietly costs $200+ a month in attention — before the server bill.

Our Starter plan is $15 for 2,000 renders. Not a subtle comparison, and that's the point: for most teams this trades a recurring engineering tax for a line item. Failed renders never bill, and the babysitting — all six items above — moves to somebody else's pager.

When building it is the right call

An honest list, because sometimes the answer really is build:

  • Volume. Past hundreds of thousands of renders a month, amortized infrastructure beats per-render pricing — provided you staff the maintenance for real instead of donating your nights to it.
  • Data boundaries. If URLs or rendered output can't leave your network for compliance reasons, hosted rendering is off the table entirely. Build.
  • Deep customization. Custom browser flags, request interception, script injection into target pages — needs beyond a hosted API's parameter surface mean you want your own browsers.
  • Existing browser infrastructure. A scraping team with fleet management already running can add screenshots at near-zero marginal cost. Their 400 lines are already written.

Everywhere else — the side project, the SaaS mailing weekly dashboard snapshots, the programmatic SEO site with ten thousand OG images — buying is arithmetic.


Shotium is the bought version: screenshots and OG images from one clean API — zombie reaping, font stack, timing heuristics, queueing, SSRF guard, and the upgrade treadmill all included. Sign-up comes with 100 free render credits, no card: enough to find out whether deleting your 400 lines beats feeding them.