How to Generate and Download Puppeteer PDFs from Firebase Functions
Build a Firebase HTTP function that renders pages with Puppeteer, returns a downloadable PDF, and avoids common Chromium and deployment failures.

Use a Firebase HTTP function to render a page with Puppeteer, call page.pdf(), and send the returned bytes with PDF download headers. Puppeteer returns a Uint8Array. Firebase HTTP functions finish by calling send(), redirect(), or end(). The complete flow is:
- Validate and authorize the request.
- Launch a Chromium executable available in the deployed function.
- Navigate to trusted content or set page HTML.
- Wait for the content required in the document.
- Call
page.pdf(). - Return the bytes as
application/pdfwith an attachment filename. - Close the browser in a
finallyblock.
Puppeteer PDF output uses the print CSS media type by default. If your page is styled for screens, call page.emulateMediaType('screen') before generating the PDF. See the Puppeteer Page.pdf() API and Firebase HTTP functions guide.
1. Create the Firebase function
Initialize a Firebase project with Functions enabled, then use a supported Node.js runtime. Firebase currently lists Node.js 20 and 22 as supported; Node.js 18 is deprecated. Set the runtime in functions/package.json:

{
"name": "functions",
"engines": { "node": "22" },
"main": "index.js",
"dependencies": {
"firebase-functions": "latest",
"puppeteer": "latest"
}
}
The standard puppeteer package downloads a compatible Chrome for Testing binary during installation. The deployment build must run the install script and preserve the browser cache in the function artifact. If your package manager disables install scripts, the browser may be absent after deployment.
Install dependencies:
cd functions
npm install
2. Implement an HTTP PDF endpoint
This handler accepts a URL from the request body, applies a small allowlist, launches Chromium, waits for the page, and returns a file download.
const { onRequest } = require('firebase-functions/v2/https');
const puppeteer = require('puppeteer');
const allowedHosts = new Set([
'example.com',
'www.example.com'
]);
exports.downloadPdf = onRequest({
memory: '1GiB',
timeoutSeconds: 120,
cors: ['https://your-frontend.example']
}, async (req, res) => {
if (req.method !== 'POST') {
res.status(405).set('Allow', 'POST').send('Use POST');
return;
}
const rawUrl = req.body && req.body.url;
let target;
try {
target = new URL(rawUrl);
} catch {
res.status(400).send('url must be a valid URL');
return;
}
if (target.protocol !== 'https:' || !allowedHosts.has(target.hostname)) {
res.status(403).send('URL is not allowed');
return;
}
let browser;
try {
browser = await puppeteer.launch({
headless: true,
args: ['--no-sandbox', '--disable-setuid-sandbox']
});
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 900, deviceScaleFactor: 1 });
await page.goto(target.href, {
waitUntil: 'networkidle0',
timeout: 90000
});
await page.emulateMediaType('print');
const pdf = await page.pdf({
format: 'A4',
printBackground: true,
margin: {
top: '18mm',
right: '15mm',
bottom: '18mm',
left: '15mm'
},
preferCSSPageSize: true
});
res.status(200)
.set('Content-Type', 'application/pdf')
.set('Content-Disposition', 'attachment; filename="document.pdf"')
.send(Buffer.from(pdf));
} catch (error) {
console.error('PDF generation failed', error);
res.status(500).send('PDF generation failed');
} finally {
if (browser) {
await browser.close();
}
}
});
The 1GiB memory and 120-second timeout are starting values, not universal requirements. Increase resources only after observing document complexity and deployed behavior. Firebase allows HTTP and callable function timeouts up to 3600 seconds, but a larger ceiling does not make a slow render reliable.
3. Deploy and download the result
Deploy the function from the project root:
firebase deploy --only functions:downloadPdf
Call the deployed endpoint with cURL:
curl -X POST \
-H 'Content-Type: application/json' \
-d '{"url":"https://example.com"}' \
-o document.pdf \
'https://REGION-PROJECT_ID.cloudfunctions.net/downloadPdf'
A Python client can save the response as bytes:
import requests
endpoint = "https://REGION-PROJECT_ID.cloudfunctions.net/downloadPdf"
r = requests.post(endpoint, json={"url": "https://example.com"}, timeout=150)
r.raise_for_status()
with open("document.pdf", "wb") as output:
output.write(r.content)
Node.js can use the built-in fetch available in current Node runtimes:
const endpoint = 'https://REGION-PROJECT_ID.cloudfunctions.net/downloadPdf';
const response = await fetch(endpoint, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ url: 'https://example.com' })
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const bytes = Buffer.from(await response.arrayBuffer());
require('fs').writeFileSync('document.pdf', bytes);
4. Browser-side downloads and CORS
Firebase HTTP functions have no cross-origin policy by default. A browser application on another origin needs an explicit allowlist, such as the cors setting in the example. Do not use a wildcard when the endpoint can render private or authenticated content.
With fetch, read the response as a Blob and create a temporary link:
const response = await fetch(FUNCTION_URL, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ url: 'https://example.com' })
});
if (!response.ok) throw new Error(await response.text());
const blob = await response.blob();
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = 'document.pdf';
link.click();
URL.revokeObjectURL(link.href);
5. Choose the page-loading strategy
Navigate to a URL
Use page.goto() when the function renders an existing page. waitUntil: 'networkidle0' waits for the network to become idle, but it can hang on analytics, streaming, or long polling. For those pages, use waitUntil: 'domcontentloaded' and then wait for a specific selector:
await page.goto(target.href, { waitUntil: 'domcontentloaded', timeout: 90000 });
await page.waitForSelector('#report-ready', { timeout: 30000 });
A fixed delay is useful for a known animation but is less precise:
await new Promise(resolve => setTimeout(resolve, 1500));
Render supplied HTML
For controlled content, use setContent():
await page.setContent('<h1>Invoice</h1><p>Paid</p>', {
waitUntil: 'networkidle0'
});
const pdf = await page.pdf({ format: 'A4', printBackground: true });
Do not pass arbitrary HTML or URLs directly from untrusted callers. A PDF renderer that can reach caller-selected addresses can become a server-side request forgery path. Restrict hosts, reject private network destinations, require authentication, and apply request size and time limits.
6. PDF options that affect output
| Option | Use |
|---|---|
format |
Preset paper such as A4 or Letter. |
width, height |
Custom paper dimensions, normally with CSS units. |
landscape |
Rotate the page orientation. |
margin |
Set top, right, bottom, and left margins. |
printBackground |
Include background colors and images. |
displayHeaderFooter |
Enable header and footer templates. |
headerTemplate, footerTemplate |
Supply HTML templates for running headers and footers. |
pageRanges |
Render selected pages, such as 1-3. |
preferCSSPageSize |
Honor @page size rules instead of scaling to a format. |
omitBackground |
Produce a transparent PDF background where supported by the rendering path. |
Use print-specific CSS to control pagination:
@page { size: A4; margin: 18mm 15mm; }
@media print {
.screen-only { display: none; }
.avoid-break { break-inside: avoid; }
h2 { break-after: avoid; }
}
Print rendering can alter colors. If exact screen colors matter, add -webkit-print-color-adjust: exact to the relevant rules and use printBackground: true.
7. Chromium packaging choices
puppeteer is simplest when its install step can download and retain Chrome. puppeteer-core does not download a browser; use it when you manage a browser yourself and provide an executable path or remote connection.
A serverless Chromium package such as @sparticuz/chromium is another strategy with puppeteer-core. Its README documents launch arguments and an executable path, and describes a packaged binary over 50 MB plus a smaller option with separately hosted assets. Check the exact Chromium and Puppeteer pairing, Linux architecture, permissions, and Firebase behavior before pinning versions. Its turnkey compatibility statements concern supported AWS Lambda runtimes and should not be presented as Firebase certification.
For Google Cloud Functions, Puppeteer’s troubleshooting guidance recommends placing the browser cache under node_modules to avoid cases where a cached build prevents installation. Confirm the result in the actual deployed artifact.
8. Reliability, performance, and cost
- Reuse only what is safe. A fresh browser per request is simple but expensive. Reusing a browser can reduce startup time, yet pages and cookies must be isolated and crashed browsers must be replaced.
- Close every resource. Use
finallyfor browsers and close pages after each job when using a shared browser. - Control concurrency. Several simultaneous Chromium pages can exhaust memory. Limit concurrent jobs at the application layer and measure deployed memory usage.
- Bound work. Set navigation, selector, and function timeouts. Avoid waiting forever for network idle on pages with persistent connections.
- Reduce output size. Optimize source images and avoid loading unnecessary resources. Large PDFs increase response time and memory pressure.
- Choose storage for durable files. Returning bytes is convenient for direct downloads. For large or repeatedly accessed documents, generate the file asynchronously and store it in Cloud Storage, then return a controlled link. The reviewed documentation does not define a universal PDF-size threshold.
- Account for serverless billing. Memory and CPU settings affect function cost. Firebase’s documented timeout values are ceilings, not expected generation times or benchmarks.
9. Troubleshooting
“Could not find Chrome” or an executable-path error
The browser was not installed, the cache was omitted, or puppeteer-core has no configured executable. Ensure install scripts run, keep the cache in the deployed artifact, or configure a managed Chromium path explicitly.

The function times out during navigation
The page may keep connections open, block the function region, or wait for a resource that never finishes. Replace networkidle0 with domcontentloaded plus waitForSelector, and set a bounded timeout.
The PDF is blank or missing images
Capture may happen before client-side rendering or lazy images complete. Wait for a readiness selector, call page.evaluate only for controlled pages, and verify image URLs and fonts are reachable from the deployed region.
Styles or colors differ from the browser
page.pdf() uses print media. Add print CSS, call emulateMediaType('screen') when appropriate, and enable printBackground. Use preferCSSPageSize when your @page rules define the intended paper.
The browser request fails with CORS in the frontend
Add the exact frontend origin to the function’s CORS configuration. A cURL request can succeed while a cross-origin browser request is blocked.
Deployment succeeds but runtime crashes
Check Node.js version, Linux architecture, executable permissions, package size, memory, and the Puppeteer/Chromium pairing in the deployed environment. Local success does not prove deployed compatibility.
Private data appears in an exported document
Make authorization explicit. Do not let callers choose arbitrary internal URLs, forward unrestricted cookies, or expose a public renderer without host and network controls.
Or skip the browser setup
If you only need a clean screenshot or PDF from a URL, ScreenshotNeo provides a single request instead of packaging Chromium in Firebase. Read the ScreenshotNeo API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server lets Claude, Cursor, and other MCP clients take screenshots with take_screenshot, inspect pages with get_page_info, and create PDFs with capture_pdf. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.
FAQ
Does page.pdf() return a file path?
No. It returns PDF bytes as a Uint8Array. Convert them to a Node.js Buffer and send or store them.
Can I use screen CSS in the PDF?
Yes. Call page.emulateMediaType('screen') before page.pdf(), then decide whether backgrounds should be printed.
What Firebase timeout should I choose?
Set a bounded timeout based on observed document complexity. Firebase permits HTTP functions up to 3600 seconds, but that is a maximum configuration value, not a recommended render duration.
Should every request launch a new browser?
It is the simplest isolation model. A shared browser can improve startup performance, but requires page isolation, concurrency limits, and recovery when Chromium crashes.
When should I store the PDF instead of returning it?
Return bytes for an immediate download. Use durable storage when users need a reusable link, retries, asynchronous processing, or repeated access to a large document.


