Stop Maintaining Your Own PDF Rendering Stack

You shipped a PDF feature. Maybe it was invoices. Maybe contracts, reports, or shipping labels. You picked Puppeteer, spun up a headless Chromium instance, wrote some HTML, and called page.pdf(). It worked. You moved on.

That was six months ago. Now you're debugging memory leaks at 2 AM, your Docker image is 1.8 GB, and you just spent a week figuring out why a Thai font renders as boxes in production but looks fine on your Mac.

You didn't sign up to run a browser fleet. But here you are.

The hidden cost nobody talks about

The initial build is cheap. A few days, maybe a week. Puppeteer's API is clean. You get pixel-perfect PDFs from HTML. It feels like a win.

Then production happens.

The real cost isn't the build. It's the ongoing tax you pay every month to keep the thing running. And that tax is invisible in your sprint planning because it shows up as "incidents", "infra work", and "that thing only Carlos knows how to fix."

Let's break it down honestly.

Memory leaks and zombie processes

Chromium was built to render web pages for humans. It was not built to run as a headless service processing thousands of requests per hour inside a container.

Every Puppeteer instance spawns a full Chromium process. Each one eats 200-400 MB of RAM. If you're not careful about closing pages and browser contexts, that memory never comes back. You'll see your container's RSS climb steadily until the OOM killer steps in.

The fix? You write a process manager. You add health checks. You set up periodic restarts. You implement request queuing so you don't spawn 50 browsers at once. Congratulations, you've just built a browser orchestration layer. That wasn't on the roadmap.

And then there are the zombie processes. A render times out, the page crashes, or the process gets a signal it doesn't handle. Now you've got orphaned Chromium processes eating CPU and memory. You'll write a cleanup script. You'll add it to a cron job. You'll forget about it until it stops working.

Cold starts will hurt you

If you're running on serverless (Lambda, Cloud Run, Cloud Functions), the cold start problem is brutal. A Chromium binary takes 3-5 seconds to launch. Your users are waiting for an invoice PDF. Five seconds feels like forever.

So you start keeping warm instances. Or you move to always-on containers. Or you build a pre-warming strategy. Each solution adds complexity and cost. The serverless pricing advantage disappears fast when you're paying for idle containers just to avoid cold starts.

On Kubernetes, it's a different flavor of the same problem. Your pods take 30+ seconds to become ready because they need to download and initialize Chromium. Autoscaling is sluggish. Traffic spikes hit before new pods are warm. Users get timeouts.

The Docker image problem

A minimal Node.js image is around 50 MB. Add Chromium and its dependencies, and you're at 1.2-1.8 GB.

That means slower deployments. Slower CI/CD pipelines. More storage costs. Longer pull times when scaling out. And every time you update your base image, you're playing dependency roulette with Chromium's system library requirements.

You'll find yourself pinning specific Chromium versions because the latest one broke something. Then you're running an outdated browser with known CVEs, and your security team is filing tickets.

Font hell is real

Your HTML template looks perfect locally. You've got system fonts, Google Fonts loaded via CDN, everything renders beautifully. Then you deploy to a Linux container that has exactly three fonts installed, and your invoices look like ransom notes.

The fix is installing fonts in your Docker image. But which fonts? Your designers keep changing the templates. Different clients need different languages. CJK fonts alone add 100+ MB to your image.

You'll set up a font management system. You'll create a shared volume. You'll write documentation that nobody reads. Six months later, someone adds Arabic text to a template and everything breaks again because you forgot to install a right-to-left font.

Chromium updates: the gift that keeps giving

Google ships a new Chromium version roughly every four weeks. Each update can change rendering behavior. A CSS property that worked before might render differently. Your pixel-perfect PDFs are suddenly off by a few pixels, and your automated tests don't catch it because they're comparing file sizes, not visual output.

You either pin your Chromium version and accumulate security debt, or you update regularly and deal with rendering regressions. There's no good option.

What this actually costs

Let's put rough numbers on it. These are conservative estimates based on a mid-size team running Puppeteer in production:

Initial setup: 1-2 weeks of dev time. This is the easy part.

Monthly maintenance: 2-4 hours per month on average. Some months it's zero. Some months it's a full week when something breaks badly.

Incident response: 1-2 incidents per quarter that wake someone up or disrupt a sprint. Each one costs 4-8 hours of focused debugging.

Infrastructure overhead: Extra container resources (CPU, RAM), longer CI/CD times, larger images. Roughly $50-200/month in cloud costs you wouldn't otherwise have.

Opportunity cost: This is the big one. Every hour your team spends on PDF rendering infrastructure is an hour not spent on your actual product. Over a year, that's easily 80-120 hours of engineering time. At $150/hour fully loaded, that's $12,000-18,000 per year on something that isn't your core business.

And that's if things go well. One bad incident, one tricky scaling problem, one "we need to support right-to-left languages by Friday" request can blow those numbers up fast.

Move the rendering engine out of your stack

The pattern here is the same one we've seen play out with email, SMS, and payment processing. You can run your own SMTP server. You can integrate directly with carrier APIs. You can build your own payment flow. But at some point, the maintenance cost exceeds the benefit, and you hand it off to a service that does one thing well.

PDF rendering is at that inflection point for most teams. Unless PDFs are your core product, you're better off treating rendering as an external service.

What you actually want is simple: send data in, get a PDF back. No browsers to manage, no fonts to install, no containers to tune.

An API call instead of a browser fleet

With Transactional.dev, you design your templates in HTML and Tailwind with Handlebars variables, conditions, and loops. Then you make a single API call with your data, and you get a PDF back.

curl -X POST https://api.transactional.dev/v1/generate \
  -H "x-api-token: YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "template": "invoice-v2",
    "data": {
      "company": "Acme Corp",
      "items": [
        {"description": "Widget", "qty": 5, "price": 29.99},
        {"description": "Gadget", "qty": 2, "price": 49.99}
      ],
      "total": 249.93
    }
  }'

No Chromium process. No Docker image bloat. No font management. Google Fonts are included. You get a monitoring dashboard instead of container logs. The mental model is the same as transactional email: maintain templates, pass data, the service handles the rest.

When to keep your own stack

To be fair, there are cases where running your own rendering makes sense:

You need offline generation. Air-gapped environments with no external API calls require self-hosted.

You have extreme rendering requirements. If you're injecting custom JavaScript that manipulates the DOM at render time, or doing things that require full browser lifecycle control, you may need the flexibility of Puppeteer.

PDFs are your core product. If you're building a PDF tool, you probably need to own the rendering layer.

For most SaaS teams building invoices, receipts, contracts, or reports alongside their main product, none of these apply. You're not in the PDF business. You just need PDFs to exist.

The real question

The question isn't "can we run Puppeteer in production?" You clearly can. Teams do it every day.

The question is whether you want to keep paying the maintenance tax, or whether you'd rather spend that engineering time on something that actually moves your product forward.

If you want to avoid maintaining your own PDF rendering stack, Transactional.dev gives you a template-based PDF API that works like transactional email. You can start with the free tier and generate your first PDF from an HTML template in a few minutes.