Guides · Puppeteer serverless HTML to PDF timeout fix
Puppeteer Serverless HTML to PDF Timeout Fix
If you searched for Puppeteer serverless HTML to PDF timeout fix, you have probably already hit one of these:
TimeoutError: Navigation timeout of 30000 ms exceeded- Lambda / Cloud Functions killed at 10s or 15s
- Chromium failed to launch (
libnss3, missing fonts, read-only/tmp) - Cold starts that eat the whole budget before
page.pdf()runs
This guide explains why those failures are structural — and a copy-paste way to stop running Chrome inside the function for the merge step.
Why serverless Puppeteer times out
A typical HTML→PDF function does three expensive things in one invocation:
- Download / unpack Chromium (cold start)
- Launch a browser + new page
setContent(html)+page.pdf()
On a warm container that can finish in a few seconds. On a cold one, steps 1–2 alone exceed many hobby/serverless limits. Adding more memory helps a bit; it does not remove the binary tax.
Fix pattern: split merge from print
Most “HTML to PDF” jobs are really two jobs:
- Merge — inject order/invoice JSON into an HTML template
- Print — turn that HTML into bytes (PDF)
Keep print where Chrome is already warm (a dedicated worker, a queue with a long timeout, or the user’s browser). Move merge to a fast HTTP API so your serverless handler only does JSON I/O.
DocuHook is built for that merge: data + html in, PDF-ready HTML out — typically around 200ms, no Chromium.
Before (fragile)
// Often dies on cold start / 10s limit
const browser = await puppeteer.launch({ /* chromium args… */ });
const page = await browser.newPage();
await page.setContent(mustache.render(template, data), { waitUntil: "networkidle0" });
const pdf = await page.pdf({ format: "A4" });
await browser.close();After (thin handler)
import { DocuHookClient } from "@docuhook/node";
const dh = new DocuHookClient({ apiKey: process.env.DOCUHOOK_API_KEY });
export async function handler(event) {
const data = JSON.parse(event.body);
const result = await dh.render(data, INVOICE_HTML, { filename: "invoice.pdf" });
// result.html is merged — ship to S3, email, or a print worker
return {
statusCode: 200,
headers: { "Content-Type": "text/html; charset=utf-8" },
body: "html" in result ? result.html : JSON.stringify(result),
};
}Install:
npm install @docuhook/nodeAsync when the webhook cannot wait
Outbound SaaS webhooks often demand a fast 2xx. Do not launch Puppeteer in that request. Enqueue:
const job = await dh.render(data, INVOICE_HTML, { mode: "async" });
// 202 — poll dh.jobStatus(job.jobId) from a cron/workerOr POST directly to https://docuhook.com/api/v1/hooks/render with Authorization: Bearer dh_….
If you still need a PDF binary
Options that stay timeout-safe:
- Return merged HTML and let the client print / use
html2pdf.js - Push
result.htmlto a warm print pool (one long-lived Chromium, not per-request launch) - Store HTML + generate PDF overnight in a batch job with a 5-minute timeout
The serverless edge function no longer carries Chromium.
Checklist
- Remove
@sparticuz/chromiumfrom the hot path if merge was the only reason you needed it - Keep templates under your size limits; send
data+htmleach request (stateless) - Use sync for interactive APIs; async for bursts
- Watch plan PDF quotas at docuhook.com/plans
The 3-line fix
Replace the Puppeteer launch/merge block with:
const dh = new DocuHookClient({ apiKey: process.env.DOCUHOOK_API_KEY });
const { html } = await dh.render(payload, templateHtml);
// ~200ms — no Chromium cold start, stays under serverless timeoutsSandbox (no signup): docuhook.com · API key: register.