The Problem Every SaaS Eventually Faces
At some point, every B2B application needs to generate PDFs. Invoices, contracts, reports, certificates - the use cases are endless. And the question always comes down to: do we build this ourselves or pay for a service?
Both paths have legitimate trade-offs. This post breaks down the real costs, complexities and decision criteria from an engineering perspective.
The Build Approach: Puppeteer + Headless Chrome
The most common DIY approach is rendering HTML in headless Chrome via Puppeteer (or Playwright) and calling page.pdf(). It sounds simple until you actually ship it.
The Initial Implementation
import puppeteer from 'puppeteer';
async function generatePdf(html: string): Promise<Buffer> {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setContent(html, { waitUntil: 'networkidle0' });
const pdf = await page.pdf({
format: 'A4',
margin: { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' },
printBackground: true,
});
await browser.close();
return pdf;
}
This works in development. Then you deploy and discover the real engineering challenges.
What You Actually End Up Building
Browser pool management - Launching a browser per request is too slow. You need a pool of warm browser instances, health checks for zombie processes and graceful restarts when Chrome crashes (and it will crash).
Memory management - Headless Chrome consumes 200-500MB per instance. Under load, your server runs out of memory fast. You need horizontal scaling, container orchestration and memory limits.
Font handling - Your local machine has system fonts. Your Docker container does not. You need to bundle fonts, handle font-face declarations and ensure consistent rendering across environments.
Asset loading - Images, stylesheets and web fonts need to be accessible to the headless browser. If your HTML references external URLs, you deal with network timeouts, CORS and broken images in production.
Page breaks - CSS page-break properties are unreliable. Content gets cut mid-sentence, tables split awkwardly and headers repeat inconsistently. You end up writing custom pagination logic.
Concurrency - 10 simultaneous requests means 10 Chrome instances. At 300MB each, that's 3GB of RAM for a modest load. You need request queuing, backpressure and timeout handling.
Monitoring - Puppeteer fails silently. A blank page generates a valid PDF. You need output validation, error reporting and retry logic.
The True Cost
A "quick" Puppeteer implementation typically takes 1-2 weeks to build, then 2-4 weeks of hardening for production reliability. Ongoing maintenance costs 5-10 hours per month dealing with Chrome updates, memory leaks and edge cases.
At an average engineering cost of $150/hour, you're looking at $30,000-$60,000 in the first year for a reliable PDF pipeline. And that doesn't include infrastructure costs for running Chrome instances.
The Buy Approach: PDF Generation APIs
Commercial PDF services abstract away the infrastructure complexity. You send HTML (or a template + data) and get a PDF back.
Popular Options
HTML-to-PDF services (Gotenberg, DocRaptor, PDFShift) - You provide fully rendered HTML. The service handles the Chrome/rendering layer.
Template-based services (Quaterio, Carbone, PSPDFKit) - You design a template visually or in code, then pass variables to generate documents.
Low-level libraries (pdf-lib, PDFKit) - Full programmatic control but you draw everything manually. No HTML rendering.
Trade-offs of Buying
Pros:
- Zero infrastructure to manage
- Consistent output without debugging Chrome quirks
- Scales automatically with usage
- Font and asset handling built in
- Usually cheaper than engineering time at low-to-mid volume
Cons:
- Monthly cost that scales with volume
- Vendor lock-in on template format
- Less control over rendering specifics
- Data leaves your infrastructure (unless self-hosted)
- Latency from network round-trips
Comparison Table
| Factor | Build (Puppeteer) | Buy (API Service) |
|---|---|---|
| Initial setup time | 2-6 weeks | Hours to days |
| Monthly maintenance | 5-10 hours | Near zero |
| Cost at 1K PDFs/month | ~$200 infra + eng time | $50-200 |
| Cost at 100K PDFs/month | ~$2,000 infra + eng time | $500-2,000 |
| Rendering consistency | You own the bugs | Service's responsibility |
| Template design | Code HTML by hand | Visual editor or code |
| Scaling | Manual (containers, pools) | Automatic |
| Data privacy | Full control | Depends on vendor |
| Customization | Unlimited | Limited by API surface |
When Building Makes Sense
Build your own pipeline if:
- You generate millions of PDFs per month (cost savings at extreme scale)
- Your PDFs require custom rendering that no service supports
- Regulatory requirements prevent data from leaving your infrastructure
- PDF generation is your core product (not a supporting feature)
- You have dedicated infrastructure engineers with capacity
When Buying Makes Sense
Use a service if:
- PDF generation supports your product but is not the product itself
- You need reliable output without investing in infrastructure
- Your team is small and engineering hours are expensive
- You want non-engineers to manage templates
- You need features like pagination, headers/footers and page numbers without building them
Where Quaterio Fits
Most PDF APIs force a choice: either you write HTML by hand and send it to a rendering service, or you use a rigid template system with limited design control.
Quaterio combines both approaches:
-
Visual template editor - Design your document layout with a WYSIWYG editor. Real pagination, headers, footers, columns and page numbers are all configurable visually.
-
Template variables - Add placeholders (text, HTML, markdown, QR codes, barcodes) that get replaced at generation time.
-
API generation - Call the API with your template ID and variable data:
const response = await fetch('https://quaterio.com/api/v1/generate/template', {
method: 'POST',
headers: {
'Authorization': 'Bearer q_your_api_key',
'Content-Type': 'application/json',
},
body: JSON.stringify({
templateId: 'invoice-template-id',
variables: {
customer_name: 'Acme Corp',
invoice_number: 'INV-2026-0142',
line_items: '<table>...</table>',
},
}),
});
const { url } = await response.json();
// url -> downloadable PDF
- Batch generation - Generate up to 1,000 PDFs in a single API call for bulk operations like monthly invoicing.
You get the design flexibility of building your own templates with the operational simplicity of a managed service.
Making the Decision
The honest answer is that most teams should start with a service and only build custom infrastructure if they outgrow it. The engineering time spent on Chrome pool management and pagination bugs is almost never the best use of a small team's capacity.
If you need a PDF generation API with a visual template editor, try Quaterio. The API is available on Business plans and above, with batch generation for high-volume use cases.