HBForge
← Back to HBForge

PDF Generation Shouldn't Crash Your Server

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.

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.

forge/pdf — basic document
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

forge/pdf — tables
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

forge/pdf — embedded SVG
// 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

forge/pdf — custom fonts
// 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

forge/pdf — 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

forge/pdf — isomorphic usage
// 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:

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.