Architecture Teardown

Replace Puppeteer PDF Generation: Fix Memory Leaks & Cut 95% RAM

By Rubrol Engineering Team • 7 min read • Updated September 2026

Using Puppeteer or Playwright to generate PDFs is one of the most common architectural antipatterns in modern web development. While page.pdf() is easy to write in a quick local script, running headless Chromium inside production containers causes severe operational headaches.

The Chromium Reality: Headless Chromium was designed to browse interactive single-page web applications, not generate high-volume B2B billing documents. It allocates hundreds of megabytes of memory for DOM parsing, GPU emulation, layout caches, and JavaScript runtimes that are completely redundant for document printing.

The Four Hidden Costs of Puppeteer PDF Generation

1. Out-of-Memory (OOM) Kubernetes Crashes

Chromium does not immediately return allocated memory to the host operating system after a page is closed. Under sustained traffic or multi-page table exports, a single Chrome instance easily balloons past 1.5 GB of RAM, triggering immediate Linux OOM-killer termination of your application container.

2. Zombie Child Processes

When unexpected client timeouts or connection drops occur, Chrome child processes often remain detached in the background, consuming CPU and memory until the host pod restarts.

3. Font and Chart Race Conditions

Because Chromium renders asynchronously, developers frequently resort to fragile workarounds like await page.waitForTimeout(1000) or waitUntil: 'networkidle0' to ensure web fonts and Chart.js graphics finish rendering before snapshotting. This adds artificial seconds of delay to every user request.

4. Inability to Meet Legal E-Invoicing Standards

Chromium produces standard web-optimized PDF 1.7 files. It cannot natively generate ISO 19005-3 compliant PDF/A-3b files, nor can it embed validated XML attachments required by mandatory European invoicing laws like Factur-X, ZUGFeRD, and EN 16931.

Head-to-Head Benchmark

Production Metric Puppeteer (Headless Chromium) Gotenberg (Docker Microservice) Rubrol (Native Typst Rust)
Warm P99 Latency 1,420 ms 980 ms 8.4 ms
Cold Start Time ~2,500 ms ~1,800 ms < 1 ms
Idle Memory Footprint 420 MB 550 MB 18 MB
100 Concurrent Requests RAM 1,850 MB (High OOM risk) 1,200 MB < 32 MB
Container Image Size ~1.2 GB (with dependencies) ~850 MB < 40 MB
European E-Invoice Support None (Manual CLI pipes) None Native PDF/A-3b + Factur-X

How Rubrol Replaces Chromium in Node.js

Rubrol eliminates the headless browser entirely. Written in native Rust, it directly compiles document specifications using the Typst typesetting system. There is no V8 engine, no headless browser instance, and no asynchronous rendering race conditions.

Before: Bloated Puppeteer Code in Node.js

// Heavy Puppeteer execution: ~1,500ms, 450MB RAM
const puppeteer = require('puppeteer');

async function printInvoice(data) {
  const browser = await puppeteer.launch({
    headless: 'new',
    args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu']
  });
  const page = await browser.newPage();
  await page.setContent(renderHtml(data), { waitUntil: 'networkidle0' });
  const pdfBuffer = await page.pdf({ format: 'A4', printBackground: true });
  await browser.close();
  return pdfBuffer;
}

After: Lean Rubrol Request in Node.js

// Fast Rubrol invocation: 8.4ms, 18MB RAM
async function printInvoice(data) {
  const response = await fetch('http://localhost:8080/v1/compile', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      template: 'invoice',
      data: data,
      compliance: 'factur-x' // Built-in PDF/A-3b + XML embedding
    })
  });
  return Buffer.from(await response.arrayBuffer());
}

Ditch the Headless Browser Today

Test the Rubrol engine live in our browser studio. See sub-10ms rendering in real time.

Launch Studio Playground

Frequently Asked Questions

Can Rubrol handle dynamic charts and visual data?

Yes. Rubrol supports native vector plotting via Typst (including CeTZ) as well as direct SVG injection. Charts render as razor-sharp vector paths with zero asynchronous animation delay.

How does Rubrol deploy inside Docker and Kubernetes?

Rubrol ships as a lean, single-binary container image under 40MB. It runs as a sidecar alongside your backend application pods, communicating over local HTTP or high-throughput Unix Domain Sockets.

Does Rubrol support custom corporate fonts?

Yes. TrueType (.ttf) and OpenType (.otf) fonts are loaded directly into memory and embedded into output PDFs with full font subsetting for minimal file size.