DEV Community

Jack
Jack

Posted on • Originally published at anethoth.com

Why Most Developers Get PDF Generation Wrong (And How to Fix It)

PDF generation seems simple until it isn't. After building a PDF API that handles thousands of documents, here are the mistakes I see developers make — and better approaches.

Mistake 1: Using Puppeteer for Everything

Puppeteer spins up a full Chromium browser to render each PDF. It works, but:

  • ~150MB RAM per instance
  • 2-5 second cold starts
  • Crashes under concurrent load

Better approach: Use a purpose-built renderer like WeasyPrint (Python) or wkhtmltopdf. They handle CSS properly without the browser overhead.

# WeasyPrint: ~30ms per document, ~50MB RAM
from weasyprint import HTML
pdf = HTML(string=html_content).write_pdf()
Enter fullscreen mode Exit fullscreen mode

Mistake 2: Building HTML Templates with String Concatenation

I've seen this in production codebases:

# Please don't do this
html = "<html><body><h1>" + company_name + "</h1>"
html += "<p>Invoice #" + str(invoice_num) + "</p>"
# ... 200 more lines of this
Enter fullscreen mode Exit fullscreen mode

This is unmaintainable, insecure (XSS), and impossible to style.

Better approach: Use Jinja2 templates with proper HTML/CSS:

from jinja2 import Template

template = Template(open("invoice.html").read())
html = template.render(
    company=company_name,
    invoice_number=invoice_num,
    items=line_items
)
Enter fullscreen mode Exit fullscreen mode

Mistake 3: Not Handling Currency Formatting

Floats and money don't mix:

>>> 0.1 + 0.2
0.30000000000000004  # Great, now your invoice is wrong
Enter fullscreen mode Exit fullscreen mode

Better approach: Use integers (cents) or Decimal:

from decimal import Decimal

subtotal = Decimal("99.99")
tax = subtotal * Decimal("0.08")
total = subtotal + tax  # Exact: 107.9892
Enter fullscreen mode Exit fullscreen mode

Mistake 4: Ignoring Page Breaks

Your PDF looks great... until the content wraps to page 2 and a table row gets split in half.

/* Prevent table rows from splitting across pages */
tr {
    page-break-inside: avoid;
}

/* Force page break before totals section */
.totals-section {
    page-break-before: auto;
    page-break-inside: avoid;
}
Enter fullscreen mode Exit fullscreen mode

Mistake 5: Generating PDFs Synchronously in Request Handlers

A 500ms PDF generation blocks your entire request. Under load, your API becomes unresponsive.

Better approach: Queue PDF generation as a background task:

# Or use an API that handles this for you
import requests

response = requests.post(
    "https://documint.anethoth.com/api/v1/invoices",
    json={"vendor": {"name": "Acme"}, "customer": {"name": "Client"}, "items": [{"description": "Service", "quantity": 1, "unit_price": 99.99}]},
    headers={"X-API-Key": "your_key"}
)
pdf_bytes = response.content
Enter fullscreen mode Exit fullscreen mode

The Lazy Way: Use an API

If you don't want to deal with template engines, CSS page breaks, font rendering, and PDF libraries — just send JSON and get back a PDF:

# Free tier: 10 invoices/month, no credit card
curl -X POST https://documint.anethoth.com/api/v1/demo-invoice   -H "Content-Type: application/json"   -d '{"vendor": {"name": "Your Company"}, "customer": {"name": "Client"}, "items": [{"description": "Consulting", "quantity": 10, "unit_price": 150}]}'
Enter fullscreen mode Exit fullscreen mode

You can also convert raw HTML to PDF for free:

curl -X POST https://documint.anethoth.com/api/v1/html-to-pdf   -H "Content-Type: application/json"   -d '{"html": "<h1>Hello World</h1><p>This becomes a PDF.</p>"}'
Enter fullscreen mode Exit fullscreen mode

What PDF generation mistakes have you run into? Drop them in the comments — I bet there are patterns I haven't seen yet.

Top comments (0)