How to generate 10,000 PDFs without running out of memory
Bulk PDF generation fails in predictable ways: one browser per document, unbounded concurrency, results held in RAM. Here is the shape of a pipeline that survives.
Updated 31 August 2026
The first bulk PDF job every team writes looks the same: a loop, a browser launch inside it, and a list that collects the results. It works for 100 documents and dies at 2,000. The fixes are not clever, but they are specific.
Failure one: a browser per document
Launching Chromium costs 1–2 seconds and a few hundred megabytes. Do that 10,000 times and you have spent three hours starting browsers. Keep one browser per worker process and open a fresh context per document; a context is cheap and isolates cookies and storage.
Failure two: unbounded concurrency
`Promise.all` over 10,000 renders asks the machine for 10,000 pages at once. Chromium does not refuse — it tries, swaps, and the OOM killer picks a victim, usually your web server. Use a queue with a fixed number of consumers. Four to six concurrent renders per 8 GB is a sensible start; measure and adjust.
Failure three: holding results in memory
A 300 KB PDF times 10,000 is 3 GB sitting in a list. Write each result to disk or object storage the moment it exists, keep only the path, and stream a ZIP at the end if the user wants one.
Failure four: the leak you did not cause
Long-lived Chromium processes grow. Not fast, but a browser that has rendered 5,000 pages holds more memory than one that has rendered five. Recycle the browser every few hundred renders. It costs one cold start per cycle and buys you a flat memory graph.
Failure five: one bad document poisons the batch
A page that never fires `load`, a font server that hangs, an infinite JavaScript loop. Every render needs a timeout, and a timed-out render must fail alone — logged, retried once on a fresh browser, then marked failed — without blocking the queue.
The pipeline shape
- API or web request enqueues a job per document, returns immediately
- Fixed pool of render workers, each owning one browser, each recycling it after N documents
- Every result written to storage with a TTL; the job row stores the path
- A webhook or a status endpoint tells the caller when the batch is done
If you would rather not build it
This is the pipeline PDFGeny runs. One request queues up to 100 documents:
curl -X POST https://pdfgeny.com/api/v1/batch \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{"documents":[{"template":"certificate","data":{"recipient":"Ada Lovelace"}},
{"template":"certificate","data":{"recipient":"Grace Hopper"}}]}'
Poll `GET /api/v1/batch/{id}` or receive a webhook per document; each finished document has a signed download link valid for seven days.
Try it: convert HTML to PDF in the browser, or get a free API key — 50 documents a month.
More guides
Convert HTML to PDF: six methods, and how each one breaks
Browser print dialog, wkhtmltopdf, WeasyPrint, Puppeteer/Playwright, LibreOffice, and a hosted API. What each produces, what it costs to run, and the failure you will hit first.
WeasyPrint vs Playwright for HTML to PDF: real timings and when each wins
We rendered the same invoices, reports and web pages with WeasyPrint and headless Chromium via Playwright. Cold and warm timings, memory, and the cases where one is simply the wrong tool.