Why Puppeteer Cannot Generate Legal EN 16931 E-Invoices (And What to Use Instead)
Every engineering team on earth starts with the same lazy reflex: "We'll just write HTML/Tailwind and call Puppeteer page.pdf()." It works for a while - until you encounter European B2B compliance. Here is the technical breakdown of why Chromium fails the law, why your bill is 10x too high, and what to use instead.
The "It's Just HTML" Trap
For a decade, web developers treated PDF generation as a web-page printing problem. Gotenberg, Browserless, and self-rolled Docker images spinning up Headless Chrome became the default architecture for every SaaS generating invoices.
It was always a hack. Running a 500MB web browser with a full Blink rendering tree and a V8 JavaScript engine just to put four text strings and an invoice total onto an A4 page is insane.
In 2026, this hack officially dies.
With the rollout of mandatory electronic invoicing across the European Union (EN 16931, French Factur-X, German ZUGFeRD 2.2), your PDF is no longer just a visual document. It is a dual-layer cryptographic container.
And Puppeteer cannot build it. Here are the three fatal architectural reasons why.
In France, Germany, and Poland, issuing an invalid electronic invoice can result in tax penalties of up to €15 per invoice (capped at €15,000 to €45,000 per year), plus enterprise accounting systems completely rejecting payment.
1. Chrome Has No Concept of ISO 19005-3 (PDF/A-3b)
Factur-X mandates that the visual invoice container conform strictly to ISO 19005-3 (PDF/A-3b). This standard exists so that a document can be archived for 10+ years without relying on external fonts, system colors, or dynamic browser scripts.
When you call page.pdf() in Puppeteer:
- Skia (Chrome’s 2D graphics engine) spits out arbitrary PDF 1.4 or 1.7 binaries.
- It does not enforce device-independent Output Intent dictionaries (such as standard sRGB IEC61966-2.1 ICC color profiles).
- It permits un-embedded system fonts and subsetting variations that fail strict PDF/A-3b verifiers (like VeraPDF).
- There is no flag in Chrome to output certified PDF/A-3. It simply does not exist in the Chromium source code.
2. The Root Catalog Attachment Disaster (`/AFRelationship`)
Under EN 16931, the machine-readable UN/CEFACT CII XML (factur-x.xml) cannot just be an ordinary file attachment like a zip file.
It must be attached at the PDF Catalog Root with a strictly defined Associated File (`/AF`) relationship dictionary set to /AFRelationship /Alternative:
<<
/Type /Filespec
/F (factur-x.xml)
/UF (factur-x.xml)
/AFRelationship /Alternative
/Desc (Factur-X electronic invoice)
/EF << /F 12 0 R /UF 12 0 R >>
>>
Puppeteer cannot touch low-level PDF dictionary objects. To work around this, developers build horrific Franken-stacks:
- Run Chromium to render HTML to a dirty PDF (takes 1,200ms).
- Shell out to
pdftkorqpdfto inject attachments (takes 300ms). - Run a Python script to patch the XMP metadata schema (takes 200ms).
Your latency is now 1.8 seconds per invoice. Your Docker image weighs 1.4 Gigabytes. And when 50 customers purchase at the same time, your EC2 instance runs out of memory and crashes.
3. The Compute Cost: Why You Are Burning \$500/Month on Nothing
Let's look at the cold, hard numbers comparing Puppeteer clusters against a native compiled engine like Rubrol:
| Metric | Headless Chrome / Puppeteer | Rubrol (Typst Native) |
|---|---|---|
| Cold Start / Engine Init | 800ms - 2,000ms | Sub-150ms |
| Warm Render Latency | 600ms - 1,200ms | 5ms - 25ms (40x faster) |
| RAM per Process | 450MB - 800MB (Chrome V8) | < 28MB (In-process Rust) |
| Throughput per CPU Core | ~1.2 invoices / sec | ~85 invoices / sec |
| Native PDF/A-3b Support | ❌ Impossible without post-processors | ✅ Built-in turnkey compiler |
| EN 16931 Validation | ❌ None (external tools needed) | ✅ In-memory Schematron validation |
What to Use Instead: The Rubrol Architecture
If you want sub-10ms performance and certified compliance, you need an engine designed from the ground up around Apache 2.0 Typst and native PDF packaging.
That is exactly what Rubrol is:
# Run the zero-dependency sidecar (multi-arch, ~30MB RAM footprint):
docker run -d -p 8080:8080 --name rubrol-sidecar ghcr.io/maxcomperatore/rubrol:latest
# Compile a certified Factur-X / ZUGFeRD PDF in 160ms:
curl -X POST http://localhost:8080/v1/facturx/render \
-H "Content-Type: application/json" \
-d '{
"template": "b2b_invoice",
"profile": "EN 16931",
"data": {
"invoice_number": "FA-2026-0842",
"issued_date": "2026-09-17",
"seller": { "name": "Acme Europe", "vat_id": "FR12345678901", "country": "FR" },
"buyer": { "name": "Deutsche Cloud GmbH", "vat_id": "DE987654321", "country": "DE" },
"line_items": [{ "name": "License", "qty": 1, "unit_price": 1000.00, "tax_rate": 0.20 }],
"grand_total": 1200.00,
"currency": "EUR"
}
}' --output invoice_facturx.pdf
No Chromium. No V8 garbage collector freezes. No page-break CSS bugs. Just clean, mathematical document layout that runs 40x faster on a fraction of the compute.