Why I Built a JSON-to-Image API Instead of Puppeteer

Published October 15, 2026 by Balaji

Uncategorized

Puppeteer will render a page. It will also leak memory at 3am. This is the founder note on why jsontoimg is a named-layer template API instead of a headless Chrome farm — not a product brochure.

Overview

Every developer who needs generated images hits the same fork in the road: spin up Puppeteer (or Playwright) and screenshot HTML, or pay for an image generation API. I went down the Puppeteer path first — and spent months paying for it in memory leaks, crashed workers, and quiet engineering hours. This is the story of why I gave up on the headless browser and built a JSON-to-image API instead, plus the real cost math that convinced me.

The Puppeteer Honeymoon Phase

Let me be fair: Puppeteer *feels* like the obvious choice at first. npm install puppeteer downloads a bundled Chromium, and you’re generating images within the hour:

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.setViewport({ width: 1200, height: 630 });
  await page.goto('https://example.com/og-template', { waitUntil: 'networkidle0' });
  await page.screenshot({ path: 'og-image.png' });
  await browser.close();
})();

Fifty lines of code. No API key, no monthly fee, no vendor. For a one-off script that runs on your laptop, Puppeteer is genuinely the right tool. The problem starts the moment you need to generate images in *production*, at *volume* — which is exactly when the “free browser” stops being free.

Where the Browser Stops Being Cheap

Here’s what nobody mentions in the Puppeteer tutorial: every browser instance you spawn is a full Chromium process. That has real, measurable consequences:

  • Memory: a single Chromium instance consumes roughly 80–300 MB of RAM depending on page complexity, and a render can pin a CPU core for 1–3 seconds (source: Medium architecture deep dive).
  • Memory leaks: Chromium leaks. A well-documented story from the SnapRender team — they spent four months fighting Puppeteer OOM crashes before switching to an API. Their browser containers died every 50 requests, failed 2–3% of screenshots, and required 3–4 workers with recycling just to stay alive (source: SnapRender).
  • Scaling ceilings: a single render server hits its limit at roughly 10–20 concurrent renders. Beyond that you need horizontal scaling: queues, worker pools, health checks, crash watchdogs, and retry logic — all of which you build yourself (source: Medium architecture deep dive, Urlbox on Kubernetes scaling).
  • Version drift: Chrome updates break your renders. Fonts render differently across environments. Cookie banners, network timeouts, and flaky networkidle0 waits make output nondeterministic.

The cost isn’t the Chrome license — it’s the engineering time spent keeping the browser farm alive. SnapRender’s numbers are telling: their self-hosted setup cost $240/month in infra plus 10–15 engineering hours per month just to babysit it. A cost comparison from Pagebolt estimates real-world Puppeteer operations at $15,000–20,000/month when you include incident response and maintenance at 10M+ screenshots per month (source: Pagebolt).

The Reframe: Screenshotting ≠ Rendering

The turning point for me was realizing I had the wrong mental model. Puppeteer is a browser automation tool. It’s unbeatable when you need to click through pages, fill forms, extract data, or screenshot an *existing website*.

But most “generate an image” use cases are not that. Generating an OG card for a blog post, a product banner for SKU #4,721, or a certificate for a course graduate is data-in, image-out — a rendering problem, not a browsing problem. You’re paying for a full JavaScript engine, layout engine, and network stack to draw text on a rectangle.

The moment I separated those two mental models, the design for jsontoimg wrote itself.

What jsontoimg Does Differently

Instead of pointing a browser at HTML, jsontoimg is a JSON-driven renderer:

1. Design a template — define the layout with placeholders for text, images, colors, and fonts.

2. Send JSON data — one POST request with the values that change.

3. Get an image back — a pixel-perfect PNG/JPG, rendered directly.

POST /api/v1/render?wait=60
{
  "template": "og-card",
  "format": "png",
  "layers": {
    "title": { "text": "Why I Built a JSON-to-Image API" },
    "author": { "text": "Balaji" },
    "date": { "text": "2026-07-31" }
  }
}

Because there’s no browser process, there are no memory leaks, no worker pools, no Chrome updates, and no 80–300 MB per render. A renderer is stateless and horizontally scales like any other API — batch an array of jobs, get an array of images back, or use webhooks for async delivery on large volumes. That’s the entire pitch: JSON in, image out, nothing to maintain.

When You Should Still Use Puppeteer

I don’t want this to read as “never use Puppeteer” — that would be dishonest. Keep the headless browser when:

  • You need to screenshot existing live web pages (visual regression testing, website previews, monitoring).
  • You need full browser interaction — login flows, form filling, multi-step automations before capture.
  • Image generation is a side feature and you have the infra team to own it.

If your real job is turning data into branded images — OG cards, product banners, certificates, ad creatives, personalized email graphics — a browser is the expensive way to do a simple thing. Those are exactly the use cases we’ve built out as reference workflows, from product banners at 10K SKUs to certificate generation.

Key Takeaways

  • Puppeteer is a browser automation tool, not an image rendering tool — using it for templated images means paying browser overhead for a rendering job.
  • The real cost of self-hosted rendering is engineering time: memory leak management, worker pools, scaling ceilings (~10–20 concurrent renders per server), and Chrome version drift.
  • Real-world numbers: SnapRender dropped from $240/mo + 10–15 eng hours to ~$99/mo after switching; Pagebolt estimates $15–20K/mo total at 10M+ screenshots.
  • A JSON-driven render API removes the browser entirely: stateless, deterministic, horizontally scalable, nothing to babysit.
  • Use Puppeteer for live-page screenshots and browser interactions; use a render API for templated, data-driven images.

FAQ

Is Puppeteer cheaper than an image generation API?

Only while the volume is low and the machine is already paid for. Once you add memory headroom, font packages, concurrency limits, and the on-call time for a renderer that falls over at 3am, the per-image cost of self-hosted Chrome stops being the cheap option.

What breaks first when you self-host headless Chrome?

Memory and fonts. Each render holds a browser process, so concurrency is bounded by RAM, and a font missing from the container silently changes the output rather than failing.

When should I still use Puppeteer?

When the thing you need a picture of is a real web page — full-page screenshots, arbitrary third-party HTML, or CSS you do not control. A template-to-image API is for assets you designed; a browser is for pages you did not.

What is the difference between screenshotting and rendering?

A screenshot reproduces whatever a browser happened to paint, including layout drift and missing fonts. Rendering composites named layers you defined, so the same JSON produces the same pixels every time.

Related Resources

Zoom out: How JSON-to-Image Conversion Works maps the whole template, named-layer, and render-job architecture.