How to Run PhantomJS in a Firebase Function
PhantomJS is a legacy choice for Firebase Functions. Learn the safe migration to Puppeteer, how to isolate old scripts, and when to use a screenshot API.

Short answer: PhantomJS is not a current Firebase Functions runtime. For new deployments, migrate the script to Puppeteer and use a supported Node.js runtime. Puppeteer’s Cloud Functions troubleshooting guide says the Google Cloud Functions Node.js runtime includes the system packages needed for Headless Chrome. Keep PhantomJS only for legacy maintenance, or run the old executable in a separately managed service and call it from Firebase.
Firebase currently documents Node.js 20 and 22 for Functions. Node.js 18 is deprecated, and Node.js 14 and 16 were decommissioned in early 2025. Runtime dates change, so check the Firebase runtime documentation before every long-lived deployment.
What changed from the old PhantomJS recipes?
Older tutorials commonly download a PhantomJS binary, invoke it with child_process, and deploy with an old Node version. That pattern is fragile in a managed function because the executable must match the deployed Linux environment, remain executable after packaging, fit package limits, and run on a supported runtime. Firebase’s current documentation describes JavaScript, TypeScript and Python functions, plus configurable memory, timeout, region and instance limits; it does not describe PhantomJS as a supported browser runtime.
PhantomJS itself is an obsolete browser option. Its JavaScript engine lacks many modern browser APIs, so pages that work in Chrome may fail or render differently. Treat any binary-packaging workaround as a legacy compatibility project rather than a Firebase feature.
Recommended path: migrate to Puppeteer
- Initialize or update Functions. Run
firebase init functionsin your project and choose JavaScript or TypeScript. The official setup guide is firebase.google.com/docs/functions/get-started. - Select a supported Node runtime. Set
engines.nodeinfunctions/package.jsonto a currently supported version, such as20or22, then confirm the value against Firebase’s runtime page. - Install Puppeteer. From the
functionsdirectory, runnpm install puppeteer. Pin and review the version in production so browser updates are deliberate. - Translate the page script. Replace PhantomJS calls such as
page.open,page.evaluateandpage.renderwith Puppeteer’spage.goto,page.evaluateandpage.screenshot. - Control the browser lifecycle. Launch inside the handler, close the browser in
finally, and set navigation and operation timeouts. - Exercise locally. Use the Firebase Local Emulator Suite before deploying.
- Deploy. Use
firebase deploy --only functionsafter local checks.
Complete HTTPS function in JavaScript
const { onRequest } = require("firebase-functions/v2/https");
const { setGlobalOptions } = require("firebase-functions/v2");
const puppeteer = require("puppeteer");
setGlobalOptions({ region: "us-central1", maxInstances: 10 });
exports.renderPage = onRequest(
{ timeoutSeconds: 120, memory: "1GiB" },
async (req, res) => {
const target = typeof req.query.url === "string" ? req.query.url : "https://example.com";
let browser;
try {
const parsed = new URL(target);
if (!["http:", "https:"].includes(parsed.protocol)) {
return res.status(400).json({ error: "Only HTTP and HTTPS URLs are allowed" });
}
browser = await puppeteer.launch({
headless: true,
args: ["--no-sandbox", "--disable-setuid-sandbox"]
});
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });
await page.setDefaultNavigationTimeout(60000);
await page.goto(target, { waitUntil: "networkidle2", timeout: 60000 });
const image = await page.screenshot({ type: "png", fullPage: true });
res.set("Content-Type", "image/png").status(200).send(image);
} catch (error) {
console.error("renderPage failed", error);
res.status(502).json({ error: "Page capture failed" });
} finally {
if (browser) await browser.close();
}
}
);
The --no-sandbox flags are commonly required in restricted container environments. Review your deployment’s security model before adding them. Validate and restrict user-supplied URLs if this endpoint is public; otherwise it can become a server-side request forgery proxy.

TypeScript variant
import { onRequest } from "firebase-functions/v2/https";
import puppeteer from "puppeteer";
export const renderPage = onRequest(
{ timeoutSeconds: 120, memory: "1GiB" },
async (req, res) => {
const target = String(req.query.url || "https://example.com");
let browser: puppeteer.Browser | undefined;
try {
browser = await puppeteer.launch({ headless: true, args: ["--no-sandbox"] });
const page = await browser.newPage();
await page.goto(target, { waitUntil: "networkidle2", timeout: 60000 });
const png = await page.screenshot({ fullPage: true, type: "png" });
res.type("png").send(png);
} catch (err) {
console.error(err);
res.status(502).send("Capture failed");
} finally {
await browser?.close();
}
}
);
Mapping common PhantomJS APIs
| PhantomJS | Puppeteer | Notes |
|---|---|---|
page.open(url, callback) |
await page.goto(url, options) |
Choose domcontentloaded, load or networkidle2 deliberately. |
page.render("shot.png") |
page.screenshot({path: "shot.png"}) |
Use fullPage for the complete document. |
page.evaluate(fn) |
page.evaluate(fn) |
Most simple DOM extraction code ports directly. |
page.viewportSize |
page.setViewport() |
Include deviceScaleFactor when pixel density matters. |
page.settings.userAgent |
page.setUserAgent() |
Set it before navigation. |
Function configuration that matters
Runtime and deployment settings
Set memory high enough for Chromium and the page you render. Increase timeout for slow pages, but keep a hard upper bound. Choose a region near your users or target systems. Set minInstances only when reducing cold starts justifies the cost, and cap maxInstances to protect downstream sites and your budget. Firebase exposes these controls as per-function runtime options; see Manage functions.
Environment and secrets
The older functions.config() API is deprecated and scheduled for decommissioning in March 2027. Use parameterized configuration and Secret Manager integration described in Configure your environment. Never put API keys or authenticated cookies in source code or query strings logged by default.
Navigation and rendering choices
waitUntil: "domcontentloaded"is fast for server-rendered HTML.waitUntil: "networkidle2"is useful for client-rendered pages but can wait indefinitely on analytics or streaming connections; combine it with a timeout.- Wait for a specific selector when the page has a known readiness element:
await page.waitForSelector("main", { timeout: 30000 }). - Use request interception to block heavy images, ads or trackers only when doing so does not change the result you need.
- Set viewport, locale, timezone, cookies and authorization headers before navigation when the page depends on them.
Legacy-only: invoking PhantomJS
If migration is impossible, package the exact PhantomJS executable and script with the function, verify executable permissions during the build, and invoke it with a strict timeout. This is an isolation pattern, not official Firebase support.
const { onRequest } = require("firebase-functions/v2/https");
const { spawn } = require("node:child_process");
const path = require("node:path");
exports.legacyPhantom = onRequest({ timeoutSeconds: 60, memory: "512MiB" }, (req, res) => {
const script = path.join(__dirname, "phantom", "capture.js");
const child = spawn(path.join(__dirname, "phantom", "phantomjs"), [script], {
stdio: ["ignore", "pipe", "pipe"]
});
let output = "";
let error = "";
const timer = setTimeout(() => child.kill("SIGKILL"), 45000);
child.stdout.on("data", chunk => { output += chunk; });
child.stderr.on("data", chunk => { error += chunk; });
child.on("error", err => { clearTimeout(timer); res.status(500).send(err.message); });
child.on("close", code => {
clearTimeout(timer);
if (code !== 0) return res.status(502).send(error || `PhantomJS exited ${code}`);
res.type("text/plain").send(output);
});
});
Before using this approach, confirm the binary’s architecture, dynamic library requirements, permissions and license. A separately managed container or browser-rendering API is usually easier to patch and monitor than a bundled obsolete binary.
Testing and reliability checklist
- Run the function through the Emulator Suite with representative pages.
- Test redirects, TLS errors, very large documents, slow third-party scripts and pages requiring JavaScript.
- Record structured logs for URL, duration, browser launch failures and navigation errors; omit credentials and cookies.
- Close every page and browser in
finallyblocks. - Use idempotent task processing if a queue retries a request.
- Set concurrency and instance limits so simultaneous Chromium processes do not exhaust memory.
- Recheck Node lifecycle dates before scheduled upgrades. Google Cloud lists Node.js 22 decommissioning on 2027-10-31 and Node.js 24 on 2028-10-31; these dates are volatile.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
spawn ENOENT |
Binary path is wrong or the file was not packaged. | Use an absolute path based on __dirname; inspect the deployed bundle. |
Permission denied |
Executable bit was lost. | Set permissions during build and verify them locally in a Linux-like environment. |
| Browser closes immediately | Missing libraries, incompatible binary or insufficient memory. | Prefer Puppeteer on a supported runtime; increase memory and inspect startup logs. |
| Navigation timeout | Slow page, blocked request or never-ending connection. | Use a finite timeout, wait for a selector, or choose domcontentloaded. |
| Blank screenshot | Capture occurred before client rendering completed. | Wait for a known selector or application-ready signal. |
| Function deploy rejected | Deprecated Node version or invalid runtime option. | Update engines.node and confirm current Firebase limits. |
| Out-of-memory errors | Large pages or too many concurrent browsers. | Increase memory, cap instances, block unnecessary resources and close browsers reliably. |
Performance, reliability and cost
Browser startup is usually the largest fixed cost in a request. Reuse is difficult in serverless functions because instances can be recycled, so optimize page work instead: block nonessential resources, avoid full-page screenshots when an element is enough, and wait for the smallest reliable readiness condition. Larger memory allocations can provide more CPU during startup but increase per-invocation cost; consult your Firebase and Google Cloud billing for current rates.
Cold starts, third-party outages, bot checks and rate limits remain possible with either Puppeteer or PhantomJS. Add retries only for transient failures, with backoff and an overall deadline. Do not retry invalid URLs or authentication failures. Store large screenshots in object storage rather than returning them through a function response when payload limits are a concern.
Or skip the browser setup
ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns PNG, JPEG, WebP or PDF. It accepts consent banners as a visitor and removes more than 60 known consent platforms, newsletter popups and chat widgets before capture; each cleanup step can be disabled. Bot checks, blank pages, timeouts, failed loads and cache hits are not billed, and response headers identify the page verdict and billing status.
See the ScreenshotNeo API documentation for all options, including full-page capture with lazy images, CSS element capture, device presets, custom viewport and retina scale, PDF settings, custom CSS and JavaScript, clicks, selector waits, network blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, TTL caching, signed links, asynchronous jobs, webhooks, bulk capture and usage reporting.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python
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)
Node.js
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 includes an MCP server with take_screenshot, get_page_info and capture_pdf tools for Claude, Cursor and other MCP clients. The free plan includes 1,000 shots per month without a card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
FAQ
Can I deploy PhantomJS directly with Firebase?
There is no current Firebase runtime option for PhantomJS. A bundled executable may work only as an unsupported legacy arrangement and can fail after runtime or packaging changes.

Does Puppeteer require a custom Docker image?
For Google Cloud Functions’ Node.js runtime, Puppeteer documents that the runtime includes the system packages needed for Headless Chrome. You still need to manage memory, timeout and browser lifecycle.
Should I use Python instead?
Firebase supports Python Functions, but a Node-based migration is usually simpler when replacing PhantomJS APIs with Puppeteer.
How do I keep an old script alive while migrating?
Put the binary behind a separately managed container or rendering service, call it from a small Firebase function, and migrate page operations incrementally.


