Skip to content

HTML to PDF timeouts on large pages: why they happen and how to fix them

A 200-row report renders fine, a 20,000-row one times out or kills the browser with an out-of-memory error. Large HTML to PDF jobs fail for a handful of specific reasons, and each has a clean fix. Here is why big pages time out, why the renderer crashes, and how to render heavy reports reliably.

Your invoice renders in half a second. Your quarterly report, the one with twenty thousand line items and a dozen charts, times out, or worse, the renderer dies with an out-of-memory error and takes the request down with it. Large HTML to PDF jobs fail in ways small ones never hint at, and the failures look random until you know the handful of specific causes behind them. Each one has a clean fix. Here is what actually goes wrong on big, data-heavy pages and how to render them reliably.

The short version

Large HTML to PDF renders fail for three main reasons: a wait condition like networkidle never settles because the page keeps making requests, so the navigation times out; the HTML payload is too big to push into the browser, which crashes the transfer; or the page is heavy enough that laying it out exhausts memory and the renderer is killed. The fixes, in order, are to wait on an explicit "ready" marker instead of network silence, to load very large HTML from a file or URL rather than shoving it over the wire, and to raise the timeout and memory headroom or split the report into batches when it is genuinely enormous.

Why the navigation times out before it even prints

The most common large-page timeout has nothing to do with size and everything to do with the wait condition. Many setups navigate with waitUntil: "networkidle0", which resolves only when there have been no network connections for half a second. That is fine for a static page, but a real dashboard often never goes quiet: it polls for updates, streams analytics beacons, or keeps a websocket open. The renderer waits for a silence that never comes and eventually throws a timeout, even though the page was visually ready long ago.

The fix is to stop waiting on network silence and start waiting on a signal you control. Load with the cheaper load or domcontentloaded condition, then wait for an explicit marker your page sets once its data and charts are truly done, and bump the timeout for good measure.

await page.goto(url, { waitUntil: "load", timeout: 120000 });
// wait for a marker your app adds when the report is fully built
await page.waitForSelector("#report-ready", { timeout: 120000 });
await page.pdf({ format: "Letter", printBackground: true });

Setting #report-ready yourself, after the last row is drawn and the last chart has finished, turns a guess into a fact. This is the same pattern used for charts and fonts, covered in depth in the wait for content before HTML to PDF guide. If a report is generated on a schedule from warehouse data, it is also worth making sure the numbers feeding it are complete before the render fires; teams that render nightly reports often monitor the freshness and completeness of that data so a render never captures a half-loaded table.

Why huge HTML crashes the browser outright

A different failure appears when you build the entire report as one giant HTML string and hand it to the browser with setContent. Once that string gets very large, tens or hundreds of megabytes, pushing it into the browser over the DevTools protocol becomes unreliable and can crash the transfer before rendering even starts. The page never loads because the content never fully arrived.

The robust workaround is to avoid the large in-memory transfer entirely. Write the HTML to a temporary file and point the browser at it with a file:// URL, or serve it from a local endpoint and navigate to that URL. The browser then streams the file itself instead of receiving one enormous message, which is far more stable. Teams rendering very large reports have used this to push documents well past a hundred megabytes that would reliably crash when passed as a string.

import { writeFileSync } from "fs";

writeFileSync("/tmp/report.html", bigHtml);
await page.goto("file:///tmp/report.html", { waitUntil: "load" });
await page.pdf({ path: "/tmp/report.pdf", format: "Letter" });

Writing the PDF straight to a file with the path option rather than buffering it in memory helps on the output side for the same reason: a 300-page PDF you hold in a variable is 300 pages of memory pressure you do not need.

Why the renderer runs out of memory, and what to do

The third failure is raw weight. A page with tens of thousands of rows, large embedded images, or many complex charts can take enough memory to lay out that the browser process is killed by the operating system, the classic out-of-memory crash. Throwing more RAM at it helps a little but is not a real fix, because the memory a full page layout needs grows with the content.

Two things make a genuine difference. First, give the container the headroom Chromium expects: in Docker, the default /dev/shm is tiny, so either enlarge it or launch with --disable-dev-shm-usage so Chromium writes to disk instead of a cramped shared-memory segment. That single flag resolves a surprising share of "renderer crashed" reports in containers. Second, and more importantly, question whether one PDF really needs to be one render. A report of ten thousand invoices does not have to be a single monstrous page; splitting it into batches of a few hundred, rendering each, and stitching or delivering them separately keeps every individual render small, fast and safe. Our bulk HTML to PDF page covers that batching pattern for high-volume jobs.

A word on version regressions

Not every large-page timeout is your fault. Specific Chrome releases have shipped bugs where printing a PDF times out on pages that rendered fine on an earlier version. If a report that worked last month suddenly times out after a browser update and nothing else changed, pin the browser version or test an adjacent one before you spend a day rewriting your wait logic. This is one more reason teams tire of running their own headless Chrome: you inherit its regressions on its schedule.

Getting it right through an API

A rendering API moves the whole class of problem off your servers. The service runs the browser, so an out-of-memory crash is its capacity problem, not a downed request in your app. You keep control of the parts that matter, a generous render timeout, a wait signal for when the report is ready, and the file streamed back rather than held in memory.

curl https://api.sitepdf.com/v1/render \
  -H "Authorization: Bearer $SITEPDF_KEY" \
  -F url=https://app.example.com/reports/q2-2026 \
  -F wait_for="#report-ready" \
  -F timeout=120 \
  -F format=Letter \
  -F archive=true

Because the browser fleet is managed and scaled for you, a heavy report that would kill a single self-hosted instance is just another job. The full parameter list is on the PDF generator API page, and the Puppeteer alternative page compares running the browser yourself against offloading it. For the related timing issues, see the wait for content guide and the page breaks guide for keeping long tables clean across pages.

A quick checklist

SymptomFix
Navigation times out on a live dashboardWait on a #ready marker, not networkidle
Browser crashes when passing huge HTMLLoad from a file or URL, not one giant setContent string
Renderer killed by out-of-memoryEnlarge /dev/shm or use --disable-dev-shm-usage
Report is simply enormousSplit into batches and render each separately
Worked last month, times out nowCheck for a Chrome version regression; pin the version

Wait on an explicit ready signal, avoid pushing giant HTML over the wire, give Chromium memory headroom, and batch anything truly huge. With those in place, large, data-heavy reports render as reliably as a one-page invoice.

Written by the team building Sitepdf, an HTML to PDF API that archives every page it renders. The in-browser converter is free to try; early access locks the launch pricing.

§ Early access

Get on the early-access list

The API opens to the list first, in order. Early access locks the planned launch rates for 12 months. No card required, launching soon.

Render + archive, one API