Migrating from Self-managed headless Chrome / Already HTML and CSS
Retiring your own headless browser fleet for a render API
A browser you built infrastructure around, where the document does not change at all and what you are deleting is a category of operational work.
What ports and what does not
The document, entirely. What you are migrating is everything you built around it: the container image with the shared libraries the browser needs, the process supervision, the memory limits, the concurrency cap, the zombie process reaper, the temporary directory sizing and the patching cadence for a browser that ships security updates continuously. None of that has an equivalent on the other side, because none of it is your problem afterwards.
What the call becomes
Whatever your service exposed becomes a POST to /v1/pdf. If you built an internal HTTP service around the browser, the migration is mostly a change of base URL and a change of option names, and the internal service can often stay in place as a thin adapter during the cutover, which is the safest way to do it. If you were driving the browser in-process, it is the Puppeteer or Playwright migration with your own wrapper in between.
Side by side
The old shape and the new one, with the parts that have no counterpart called out in comments rather than quietly omitted.
// A cutover pattern: keep your internal interface, change what is
// behind it. This lets one call site change at a time and gives you a
// switch back.
async function renderPdf(html, opts = {}) {
if (process.env.RENDER_BACKEND === "api") {
const res = await fetch("https://api.pdfpipe.xyz/v1/pdf", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PDFPIPE_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ html, options: toApiOptions(opts) }),
});
if (!res.ok) throw new RenderError(res.status, await res.text());
return Buffer.from(await res.arrayBuffer());
}
return renderWithLocalBrowser(html, opts); // the existing path
}
/* What gets deleted once the switch is flipped for good:
the browser and its shared libraries from the image
the process pool and its supervision
the concurrency semaphore
the zombie reaper
the shared memory sizing for the container
the browser patching cadence
That list, not the call change, is the migration's value. */The CSS delta, in both directions
What the old engine accepted that a current one does not, and what it refused that a current one wants. The second direction is the one people forget, and it is where the value of the migration is.
- None. It is the same kind of engine.
- Check the defaults you had set explicitly in your wrapper, because those are the ones most likely to differ from the defaults on the other side.
What changes without anything erroring
The dangerous list. Each of these produces a different document and no diagnostic, so none of them are caught by a test that only checks the render succeeded.
- Backgrounds, if your wrapper relied on the browser default rather than setting print_background explicitly.
- Concurrency behaviour. Your fleet had a fixed capacity and queued or failed beyond it; a hosted API has different limits and different behaviour at them, which is an operations change rather than a document one.
- Cold start characteristics, which disappear as a category and are replaced by network latency.
What to diff before cutting over
Run both backends behind the same interface for a period and compare their outputs on live traffic rather than on fixtures. That is the advantage of the adapter pattern: you get a real diff on real documents before committing, and you keep a switch back.
The one thing that always breaks
Nothing in the document, and something in the deployment. The container image loses its browser and everything that referenced it, and the first thing to fail is usually a health check, a warm-up hook or a startup probe that was waiting for a browser to be ready. It is a small failure and it happens at the worst moment, which is the first deploy after the switch.
Frequently asked
Do I have to rewrite my templates to leave Self-managed headless Chrome?
No. The templates are HTML and CSS and they carry across. What needs attention is the delta above: the places the two engines disagree, and the configuration that used to live outside the document and now lives inside it, or the other way round.
Can I run both for a while?
Yes, and it is the safest way to do it. Put both behind one internal function that takes your data and returns bytes, switch on an environment variable, and run the new path on real traffic while the old one still serves. You get a diff on real documents rather than on fixtures, and you keep a way back that does not involve a deploy.
What about the documents already generated?
Nothing here changes them. They are files that already exist. What is worth deciding before the cutover is whether a regenerated document has to match the original byte for byte or merely say the same thing, because for anything with a legal or audit character the answer is usually to keep the original file rather than to be able to reproduce it.
Other migrations
The nearest neighbours first, then others that started from the same kind of tool, because the model matters more than the language.
Migrating off self-hosted Puppeteer, an operational migration
The same browser engine on the other side, so nothing about the document changes and everything about running it does.
Migrating off self-hosted Playwright, and the option name changes
The same shape of migration as Puppeteer, with a slightly different option vocabulary and the same absence of any CSS delta.
Migrating off wkhtmltopdf without changing what the pages look like
A command line tool built on a fork of an old browser engine, driven entirely by flags, where almost every flag you were passing has a different home on the other side.
Migrating off wicked_pdf or Snappy, the wkhtmltopdf wrappers
Language wrappers that shell out to the wkhtmltopdf binary, where the framework integration is the thing you are actually replacing and the engine change comes along with it.
Migrating off dompdf, and getting flexbox back
A pure PHP renderer supporting a subset of CSS 2.1, where the migration's real content is everything your templates could not do and had to work around.
Every migration, grouped by what you are coming from
The full list, sorted by the model the old tool used rather than by its name.
What this API actually does
One page per option and endpoint that exists, which is what the mappings above point at.
Render one of your existing documents through the playground before changing any code. That comparison is the whole of the risk assessment for this migration.