How to Prevent Puppeteer PDF Timeouts in Firebase Functions
Fix Puppeteer PDF timeouts in Firebase Functions by tracing every timeout layer, configuring runtime limits, packaging Chrome correctly, and controlling waits.

Puppeteer PDF generation in Firebase Functions can time out even when page.pdf() appears to have a generous timeout. The reason is that several independent clocks are running: browser launch, navigation, selector and network waits, PDF generation, and the Firebase function itself.
The reliable fix is to identify which layer expires, set each Puppeteer timeout explicitly, configure the Firebase runtime ceiling, package Chrome where the deployed function can find it, and always close the browser. Setting page.pdf({ timeout: 0 }) only disables Puppeteer’s PDF-operation timeout; it cannot extend the enclosing Firebase function.
1. Understand the timeout layers first
A typical PDF request has this sequence:

puppeteer.launch()starts or connects to Chrome.page.goto()loads the target document.- Your code waits for a selector, response, delay, or network-idle condition.
page.pdf()waits for fonts and renders the PDF.browser.close()releases the process before the function returns.
Each operation can consume the same finite Firebase invocation budget. Puppeteer’s PDF options document a 30,000 millisecond default timeout, and the Page API documents 30 seconds as the default for navigation and related waits. A failure close to 30 seconds usually indicates one of these operation defaults. A failure near 540 seconds, 1,800 seconds, or 3,600 seconds usually indicates the Firebase platform ceiling for the trigger type.
See the Puppeteer PDFOptions reference and Page API timeout documentation for the operation-level defaults.
2. Configure the Firebase function ceiling
Set the function timeout and memory in runtime options. HTTP and callable functions can be configured up to 3,600 seconds. Scheduled and task-queue functions can reach 1,800 seconds. Applicable event-driven and first-generation functions have a 540-second ceiling. The configured ceiling must still leave enough time for your actual request path, including cold start and cleanup.
For Firebase Functions v2, a basic HTTP handler looks like this:
const { onRequest } = require('firebase-functions/v2/https');
exports.renderPdf = onRequest(
{
timeoutSeconds: 300,
memory: '1GiB'
},
async (req, res) => {
// Generate and return the PDF here.
}
);
Deploy the function after changing runtime options, then confirm the deployed configuration in the Firebase console or with the Firebase CLI. The Firebase timeout and memory guide describes the platform limits and deployment settings. Runtime option details are in the Firebase RuntimeOptions reference.
Do not set an operation timeout equal to the platform ceiling. Reserve time for the response, logging, and browser.close(). For example, a 300-second function might use a 60-second launch timeout, a 60-second navigation timeout, and a 60-second PDF timeout, with the remaining budget available for cold starts and slower pages.
3. Make navigation and readiness waits explicit
Many “PDF timeouts” occur before PDF generation. A page may keep analytics, advertisements, WebSockets, or polling requests open indefinitely. Waiting for networkidle2 can therefore consume the whole invocation even though the page is already printable.
Choose a readiness condition that matches the application:
| Condition | Use it when | Risk |
|---|---|---|
domcontentloaded |
HTML structure is enough to begin rendering. | Images, fonts, or client-rendered data may not be ready. |
load |
Traditional page assets must finish loading. | Slow third-party assets delay the request. |
networkidle2 |
The app becomes quiet after its data requests finish. | Polling and long-lived connections may prevent idle. |
| Application selector | The app exposes a reliable “ready” element. | The selector can change or never appear on an error page. |
Set both the default navigation timeout and the timeout for the individual wait. Puppeteer uses 0 to disable these operation timeouts, but disabling a wait is safe only when an outer deadline or your own abort logic still exists.
page.setDefaultNavigationTimeout(60000);
page.setDefaultTimeout(60000);
await page.goto(targetUrl, {
waitUntil: 'domcontentloaded',
timeout: 60000
});
await page.waitForSelector('#report-ready', {
visible: true,
timeout: 60000
});
If there is no dependable selector, use a short, measured delay after domcontentloaded or expose a readiness marker in the application. Avoid using a long arbitrary delay as a substitute for understanding the page.
4. Account for fonts and PDF-specific work
page.pdf() waits for fonts by default. Remote font providers, blocked font requests, or a stylesheet that never resolves can make the PDF phase look hung. The official Puppeteer PDF guide documents this font behavior.
For predictable output:
- Self-host required fonts where practical.
- Preload important font files and use a stable
font-displaystrategy. - Wait for your application’s content-ready signal rather than all network traffic.
- Use
printBackground: truewhen the design depends on background colors or images. - Write temporary files under
/tmp, the writable directory available to Firebase Functions.
PDF options such as format, width, height, margin, landscape, displayHeaderFooter, headerTemplate, footerTemplate, pageRanges, preferCSSPageSize, and printBackground change rendering work but do not remove the enclosing function limit. Keep headers and footers simple: they are HTML fragments rendered for every page.
5. Package Chrome so launch works after deployment
A local machine may find a browser cache that is absent from the deployed artifact. Puppeteer’s Cloud Functions guidance recommends placing its browser cache under node_modules so the browser files are included with the function deployment. Follow the Puppeteer configuration guide for the cache-directory configuration supported by your Puppeteer version.
Before deploying, inspect the generated function directory and verify that the expected executable exists in the configured cache. A launch that hangs or reports “Could not find expected browser locally” is a packaging problem, not a PDF timeout. Also check that the deployed Node.js runtime, Puppeteer version, and browser revision are compatible.
Use a launch timeout so a broken executable fails clearly:
const browser = await puppeteer.launch({
timeout: 60000,
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
Use the launch arguments required by your chosen Firebase runtime and Puppeteer setup. Keep the browser cache configuration in source control or in the build configuration so local and deployed behavior stay aligned.
6. A complete Firebase v2 implementation
The following pattern gives every major operation an explicit budget, writes the PDF to /tmp, returns it as a download, and closes Chrome on success or failure.
const { onRequest } = require('firebase-functions/v2/https');
const puppeteer = require('puppeteer');
const fs = require('node:fs/promises');
exports.renderPdf = onRequest(
{ timeoutSeconds: 300, memory: '1GiB' },
async (req, res) => {
const targetUrl = String(req.body?.url || req.query.url || '');
if (!targetUrl.startsWith('https://')) {
res.status(400).send('url must be an https URL');
return;
}
let browser;
const outputPath = `/tmp/render-${Date.now()}.pdf`;
try {
browser = await puppeteer.launch({
timeout: 60000,
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
const page = await browser.newPage();
page.setDefaultNavigationTimeout(60000);
page.setDefaultTimeout(60000);
await page.goto(targetUrl, {
waitUntil: 'domcontentloaded',
timeout: 60000
});
// Replace this with an application-specific readiness check when available.
await page.evaluate(() => document.fonts.ready);
await page.pdf({
path: outputPath,
format: 'A4',
printBackground: true,
preferCSSPageSize: true,
timeout: 60000,
margin: { top: '16mm', right: '16mm', bottom: '16mm', left: '16mm' }
});
const pdf = await fs.readFile(outputPath);
res.set('Content-Type', 'application/pdf');
res.set('Content-Disposition', 'attachment; filename="document.pdf"');
res.status(200).send(pdf);
} catch (error) {
console.error('PDF generation failed', error);
res.status(500).send('PDF generation failed');
} finally {
if (browser) {
try {
await browser.close();
} catch (closeError) {
console.error('Browser cleanup failed', closeError);
}
}
}
}
);
The numeric values are starting points, not guarantees. Tune them from timestamps collected on your pages, and keep the sum below the Firebase ceiling.
7. Diagnose the exact failing layer
Add timestamps around every expensive operation. This turns a generic timeout into an actionable diagnosis:
const mark = (name) => console.log(JSON.stringify({
name,
at: new Date().toISOString()
}));
mark('launch:start');
browser = await puppeteer.launch({ timeout: 60000 });
mark('launch:end');
mark('goto:start');
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60000 });
mark('goto:end');
mark('pdf:start');
await page.pdf({ path, format: 'A4', timeout: 60000 });
mark('pdf:end');
| Failure signature | Likely cause | Fix |
|---|---|---|
Timed out after 30000 ms while waiting for... |
A Puppeteer operation is using its documented default. | Set the relevant navigation, selector, or PDF timeout explicitly. |
| Launch hangs or “Could not find expected browser locally” | Chrome is missing or the cache is outside the deployed artifact. | Configure the cache under node_modules and inspect the deployed files. |
| Timeout near 540 seconds | The trigger is subject to the 540-second platform ceiling. | Check trigger generation and type; redesign or move the workload if necessary. |
| HTTP request ends before the configured value | The deployed runtime settings differ from source, or another proxy/client deadline is shorter. | Verify the deployed function configuration and client timeout. |
| PDF waits forever at network idle | Polling, analytics, WebSockets, or an uncompleted request keeps the page active. | Use domcontentloaded plus an application-ready selector. |
| Content is missing but no timeout occurs | Rendering began before client-side data or fonts were ready. | Wait for a specific readiness signal and verify font loading. |
8. Performance, reliability, and cost considerations
- Cold starts: Browser startup and loading the browser binary consume time before navigation begins. Keep the deployment small and measure launch separately.
- Memory: Complex pages, large images, and multiple concurrent pages increase memory pressure. Firebase documents memory as a runtime option, but there is no universal memory value that guarantees every PDF workload.
- Concurrency: Do not assume one browser can safely process unlimited simultaneous jobs. Bound concurrency, monitor failures, and close pages and browsers deterministically.
- Navigation scope: Use the narrowest readiness condition that produces correct output. Loading every third-party resource increases both latency and failure surface.
- Retries: Retry transient navigation failures only when the operation is idempotent. A retry can double runtime and may push an invocation over its platform limit.
- Cleanup: Always close the browser in
finally. Orphaned processes can increase later latency and memory use. - Cost: Longer execution, higher memory, and repeated retries can increase Firebase usage. Measure document size, asset count, cold-start frequency, and concurrency before selecting runtime settings.
There is no official universal completion-time or memory benchmark for all Puppeteer PDFs. Treat the example values as an initial configuration and tune them against your own documents.
9. Or skip the browser setup
If your requirement is simply to capture a URL as an image or PDF, ScreenshotNeo provides a hosted screenshot API and MCP server. It removes cookie and consent banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and each response reports its verdict in X-Page-Verdict and X-Billed headers.

Read the ScreenshotNeo API documentation for all options. A one-call PDF request can be made with the same endpoint:
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-d format=pdf \
-o document.pdf
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={
"access_key": "YOUR_API_KEY",
"url": "https://stripe.com",
"format": "pdf",
},
timeout=90,
)
r.raise_for_status()
open("document.pdf", "wb").write(r.content)
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://stripe.com',
format: 'pdf'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = Buffer.from(await res.arrayBuffer());
require('node:fs').writeFileSync('document.pdf', data);
ScreenshotNeo supports PDF paper size, margins, landscape mode, and page ranges, along with custom CSS and JavaScript, cookies, headers, user agents, timezone, geolocation, waits, blocked resource types, caching, asynchronous jobs, signed webhooks, and bulk capture. Its MCP server gives AI agents tools named take_screenshot, get_page_info, and capture_pdf. The Free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
10. FAQ
Can I set page.pdf({ timeout: 0 }) and rely on Firebase?
You can disable Puppeteer’s PDF-operation timeout, but Firebase still terminates the invocation at its configured platform limit. Use an explicit finite timeout when you need predictable failure and cleanup.
Should I always use networkidle2?
No. It is appropriate only when the page becomes quiet reliably. Applications with polling, analytics, or persistent connections should use domcontentloaded plus an application-specific readiness signal.
Why does the same code work locally but fail after deployment?
The deployed function may not contain Puppeteer’s browser cache, or its Node.js runtime and browser revision may differ. Configure the cache under node_modules and inspect the deployment artifact.
Which Firebase timeout applies to my function?
Check the trigger type and generation. HTTP and callable functions can reach 3,600 seconds, scheduled and task-queue functions 1,800 seconds, and applicable event-driven or first-generation functions 540 seconds.
How can I reduce PDF latency without lowering quality?
Measure launch, navigation, waits, PDF rendering, and cleanup separately. Self-host critical fonts, avoid unnecessary third-party resources, choose a precise readiness condition, and bound concurrency.


