The Real Cost of Generating OG Images Yourself
Dynamic Open Graph images look like an afternoon job. Forty lines of Puppeteer, an HTML template, page.screenshot(), done. The demo works first time, which is exactly the problem: the demo is about five percent of the system you end up owning, and the other ninety-five percent never appears on the whiteboard.
"We'll just self-host it, Puppeteer is free" is a decision most teams make without writing a single number down. So let's write the numbers down.
What you're actually building
An OG image pipeline that survives production is a small rendering service with one internal customer. The screenshot call is the easy line. Around it sits:
A template layer that turns a title, author and cover image into HTML without breaking on 90-character titles, Japanese text or a missing thumbnail
Headless Chrome running somewhere, with the memory headroom Chrome actually wants rather than the amount you'd like it to want
A font stack, because server environments ship with almost no fonts and your titles will not all be ASCII
Storage and a CDN, because re-rendering on every crawler hit is how the whole thing falls over
Cache keys and invalidation, so an edited headline regenerates its card
Retries, timeouts and a queue for the day you redesign and need 5,000 cards rebuilt
Monitoring that can tell a correct image from a white rectangle
None of this is exotic. All of it is work, and none of it is your product.
The infrastructure maths, honestly
Here's where most build-vs-buy articles lose credibility: they claim the servers will cost you hundreds a month. They won't, and it's worth being straight about that.
Take a realistic content site. Five thousand pages, cards rendered on publish and on edit, call it 2,000 renders a month once caching is doing its job (an OG image is rendered once and served thousands of times, so render volume tracks content changes, not traffic). On Lambda at 2GB with a four-second average render that's 16,000 GB-seconds, which at $0.0000167 per GB-second comes to about $0.27 a month. Twenty-seven cents. A small VPS keeping a warm Chrome instead is $5 a month. Storage and CDN at this volume are pennies.
So the DIY case looks won, and if infrastructure were the whole bill it would be. The money goes somewhere else.
Where the money actually goes
The build is a week, not an afternoon
Getting from the working demo to the list above is three to five days of real engineering: fonts installed and subset, caching wired up, failure paths handled, a regeneration path for bulk edits, some way of noticing when output goes wrong. At a contractor rate of $500 to $700 a day, the pipeline costs $1,500 to $3,500 before the first card is ever shared. On a salaried team the invoice is invisible, but the week is still gone, and it was a week that didn't go on the product.
Chrome does not stand still
Chrome ships a new major version roughly every four weeks, and a rendering stack pinned to a Chromium build ages accordingly. On Lambda that means the @sparticuz/chromium and puppeteer-core version matrix, the 250MB unzipped limit and the periodic weekend where the layer goes stale and fonts silently vanish. I've written up that failure pattern in detail in replacing Puppeteer on AWS Lambda, but the short version is that the stack does not stay working on its own. It stays working because someone maintains it.
Blank images fail silently
This is the failure mode that stings. A font CDN hiccups, a CSS variable gets renamed, a deploy changes a class, and Chrome cheerfully screenshots a white rectangle. HTTP 200, file exists, file is wrong. JSON APIs fail loudly; pixels fail politely. Most teams find out from a colleague asking why the company's launch post unfurled blank on LinkedIn, and by then the platforms have cached the empty preview and you're in Facebook's sharing debugger clicking re-scrape.
Cold starts hit at the worst possible moment
If cards render on demand, the first request for a fresh URL is usually the crawler's request. Chrome booting inside a Lambda takes seconds on a good day, more when the binary extracts from a cold layer, and share crawlers are not patient. Miss the window and the platform caches "no image". The fixes are real but they're all more system: pre-render on publish, keep functions warm, put a queue in front. Each one is another moving part on the list.
The maintenance line item
Steady state, a self-hosted rendering path costs one to two engineering hours a month averaged over a year: a Chrome bump here, a font regression there, an OOM investigation when one page spikes memory. At $100 an hour that's $100 to $200 a month to keep a service alive whose entire infrastructure bill is 27 cents. For comparison, 2,000 renders a month lands on the $25 plan of a managed API, and 1,000 fits in the $9 one. The maintenance hours alone cost several times the thing they're maintaining an alternative to. That's the arithmetic that never makes it into the "Puppeteer is free" argument.
The Satori shortcut, and where it runs out
The other DIY route skips Chrome entirely. Satori, the engine inside @vercel/og, turns JSX into SVG and renders it in milliseconds at the edge. Where it fits, it's genuinely good, and cheap in a way headless Chrome never will be.
The catch is that Satori is not a browser. It supports a subset of CSS: flexbox layout only, no grid, no calc() in plenty of places, a limited property list. Fonts are loaded manually and emoji need their own glyph pipeline or they render as empty squares. I've catalogued the gaps in Satori's CSS limits. If your card is a title, an avatar and a gradient, Satori is a fine answer. The day your designer hands over the actual brand card, grid layout, custom type, the lot, you're back to real Chrome and the full cost structure above.
When building it yourself is the right call
Sometimes the DIY column genuinely wins, and it's worth naming when:
Volume is enormous and steady. At millions of renders a month, per-render API pricing dominates and owning the fleet pays for its keep
Your rendering needs are unusual: multi-step interaction before capture, authenticated pages, output an HTTP API can't express
You already run browser infrastructure for scraping or testing, with an owner whose job it is, so OG cards ride on maintenance you're paying for anyway
Compliance rules keep the render path inside your own network
If none of those are you, you're not really choosing between free and paid. You're choosing between two paid options where one of them also pages you at 2am. There's a fuller side-by-side in HTML to Image vs self-hosted Puppeteer.
What the bought version looks like
For everyone else, the entire pipeline collapses into one HTTP call. HTML in, hosted PNG out:
curl -X POST https://app.html2img.com/api/html \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"html": "<div class=\"card\"><h1>Pricing that scales with you</h1></div>",
"width": 1200,
"height": 630
}' The response is a CDN URL. Render once on publish, store the URL in your og:image tag and serve it forever; downloads don't consume credits, so a card that gets shared ten thousand times still cost one render. Fonts, Chrome versions, memory headroom and the white-rectangle problem are somebody else's uptime target. There are worked guides for wiring this into Laravel, Rails and Next.js if you want the framework-shaped version.
The maths, then. Self-hosting an OG pipeline at typical content-site volume: pennies of infrastructure, a four-figure build and a monthly maintenance cost that exceeds the entire price of not doing any of it. The servers were never the expensive part. Your time was.
Need OG images rendered from HTML without owning a browser fleet? Browse the templates gallery or read the docs to get started.