Generating PDFs in JavaScript is one of those tasks that sounds simple until you actually try it in production. Your staging environment works fine with 10 test invoices. Then production hits 500 concurrent requests on the first of the month, and your server falls over.
We have seen this exact scenario play out across multiple client projects. The root cause is always one of three libraries.
The Three Paths to PDF Pain
Path 1: Puppeteer — "Just Render HTML to PDF"
The most common approach in 2024-2026: render your document as HTML, then use Puppeteer to launch a headless Chrome instance and call page.pdf(). It works beautifully for the first request.
The problem is that every PDF generation spawns a Chrome process. Chrome consumes 80-150MB of RAM per instance. Ten concurrent PDF requests mean ten Chrome processes — that is 1.5GB of RAM just for PDF generation. On a 2GB server (common for small to medium deployments), this triggers the OOM killer. Your entire application dies, not just the PDF endpoint.
- * Puppeteer downloads a 280MB Chromium binary during installation
- * Each PDF generation spawns a ~120MB Chrome process
- * Chrome processes occasionally become zombies and leak memory
- * Serverless deployment is impractical (Lambda layer size limits)
- * Alpine Linux Docker images require additional Chromium dependencies
Path 2: PDFKit — The Memory Leak Champion
PDFKit is a venerable Node.js library for programmatic PDF creation. It generates PDFs directly without a browser, which sounds ideal. But it has a well-documented pattern of memory leaks on long-running servers. The internal buffer management does not always release memory correctly when generating large documents or processing many small documents over time.
We profiled a PDFKit-based invoice generator that ran for 72 hours in production. Memory usage climbed from 180MB to 1.2GB with no corresponding increase in traffic. The only fix was periodic process restarts — a workaround, not a solution.
Path 3: jsPDF — Browser-Only Limitations
jsPDF works well for simple client-side PDF generation. But its styling capabilities are severely limited: no CSS layout engine, no flexbox, no grid. Complex layouts require manually positioning every element with absolute coordinates. Tables require a separate plugin (jspdf-autotable). Custom fonts require converting TTF files to a JavaScript format and embedding them inline, inflating your bundle size by megabytes.
forge/pdf: A Different Approach
forge/pdf generates PDFs using a streaming architecture that processes pages incrementally. It never holds the entire document in memory. A 500-page PDF uses roughly the same memory as a 5-page PDF — because only the current page's content is buffered before being flushed to the output stream.
import { createPDF } from '@hyperbridge/forge/pdf'; const doc = createPDF({ size: 'A4', margins: { top: 60, bottom: 60, left: 50, right: 50 }, font: 'Helvetica', }); doc.text('Invoice #2024-0847', { fontSize: 28, fontWeight: 'bold', color: '#1a202c', }); doc.text('Issued: April 1, 2026', { fontSize: 12, color: '#6b7280', marginTop: 8, }); const buffer = await doc.toBuffer();
Tables That Just Work
doc.table({ headers: ['Item', 'Qty', 'Unit Price', 'Total'], rows: lineItems.map(item => [ item.name, item.quantity, formatCurrency(item.unitPrice), formatCurrency(item.total), ]), style: { headerBackground: '#1c1917', headerColor: '#ffffff', stripedRows: true, borderColor: '#e5e7eb', cellPadding: 10, }, columnWidths: ['40%', '15%', '25%', '20%'], }); // Auto page-breaks when table exceeds page height // Headers repeat on each new page automatically
SVG Support
// Embed SVG directly — logos, charts, icons doc.svg(logoSvgString, { x: 50, y: 30, width: 120, height: 40, }); // SVG paths, shapes, gradients — all rendered natively // No rasterization, no quality loss at any zoom level
Custom Fonts Without the Pain
// Register a font from buffer (TTF, OTF, WOFF2) const fontData = await readFile('./fonts/Inter-Regular.ttf'); doc.registerFont('Inter', fontData); doc.text('Styled with Inter', { font: 'Inter', fontSize: 16, }); // Font subsetting: only embeds glyphs you actually use // A 200KB font file becomes ~15KB in the final PDF
Interactive Form Fields
// Create fillable PDF forms doc.formField('name', { type: 'text', label: 'Full Name', required: true, x: 50, y: 200, width: 250, height: 30, }); doc.formField('agree', { type: 'checkbox', label: 'I agree to the terms', x: 50, y: 250, }); doc.formField('signature', { type: 'signature', x: 50, y: 300, width: 200, height: 60, });
Works Everywhere — Server and Browser
// Server: stream to HTTP response app.get('/invoice/:id/pdf', async (req, res) => { const doc = createPDF(); // ... build document ... const stream = doc.toStream(); res.setHeader('Content-Type', 'application/pdf'); stream.pipe(res); }); // Browser: download directly const doc = createPDF(); // ... build document ... const blob = await doc.toBlob(); const url = URL.createObjectURL(blob); window.open(url);
Memory Under Control
We stress-tested forge/pdf by generating 1,000 concurrent 50-page invoices with tables, images, and custom fonts. Peak memory usage: 340MB. The same test with Puppeteer peaked at 12GB before the OOM killer intervened at request 83.
The streaming architecture means forge/pdf's memory usage scales with page complexity, not document length. A 1,000-page report uses roughly the same peak memory as a 10-page report because completed pages are flushed to the output stream and garbage collected immediately.
Deployable Anywhere
Because forge/pdf is pure JavaScript with zero native dependencies, it runs everywhere your JavaScript runs:
- AWS Lambda — deploys within the 50MB limit easily
- Cloudflare Workers — fits within the 4MB limit
- Vercel Edge Functions — no binary to bundle
- Browser — client-side PDF generation without server round-trips
- Deno, Bun — no Node.js-specific native modules
No Chromium to download. No C++ bindings to compile. No platform-specific binaries. Your PDF generator should be a library, not a systems administration challenge.
HBForge is currently available exclusively for enterprise clients. Opening to the Developer Community on June 25, 2026.
Contact kr@hyperbridge.in for early access.