๐ŸŽ‰ Premium Proxies ยท 3-Day Free TrialClaim Now โ†’
Comparisons

Headless Chrome vs Headless Firefox for Scraping

Headless Chrome vs headless Firefox for scraping: new headless mode, CDP vs WebDriver BiDi, memory at concurrency, per-context proxies, and a decision rule.

S SparkProxy 0 18 min read
Share
Headless Chrome vs Headless Firefox for Scraping

Use headless Chrome for almost every scraping job, because per-context proxies, the full CDP surface, and the tooling ecosystem are all built around it; reach for headless Firefox only when a specific target's defenses are tuned on Chrome tells and you can accept the smaller feature surface.

Headless Chrome vs headless Firefox usually gets argued on the wrong axis. People compare page-load speed on a single URL, publish a bar chart, and call it done. A scraper never runs one page. It runs forty concurrent sessions on one box for six hours, each behind a different exit IP, against a target that scores your fingerprint before it serves HTML. On that job what matters is protocol surface, process model, proxy plumbing, and how rare your browser is in the target's traffic mix. This post covers those, with version numbers and links to the primary docs behind every claim.

Headless Chrome vs headless Firefox at a glance

AxisHeadless ChromeHeadless Firefox
EngineBlink + V8Gecko + SpiderMonkey
Headless flag`--headless` (new mode since Chrome 112)`-headless` or `MOZ_HEADLESS=1`
Legacy lightweight mode`chrome-headless-shell`, separate binary since 132none, one implementation only
Primary automation protocolChrome DevTools ProtocolWebDriver BiDi, plus Marionette for classic WebDriver
Playwright driverstock Chromium over CDPpatched Firefox build with the Juggler component
Puppeteer default protocolCDPWebDriver BiDi
Per-context proxyyes, with credentialsyes under Playwright 1.46+, not under Puppeteer
Worldwide browser sharearound 65%under 3%
Stealth patch ecosystemlarge, Chrome-shaped, ages fasteffectively none
Process model tuningcommand line flags`about:config` prefs

That table is the summary. The rest is why each row is true, and which rows should change your decision.

Chrome's headless is two browsers now, and you must pick

For most of headless Chrome's life, "headless" was not Chrome. Google's own documentation is blunt about it: headless mode "was a separate, alternate browser implementation that happened to be shipped as part of the same Chrome binary" and "didn't share any of the Chrome browser code." That one fact explains a decade of detection posts. Old headless behaved differently from real Chrome because it was a different browser.

Three dates matter, all from the Chrome Headless mode documentation and the removal announcement:

  • Chrome 112 (2023) shipped --headless=new. This runs real Chrome, creating platform windows but never displaying them. Same browser code path as headful.
  • Chrome 132.0.6793.0 (early 2025) dropped the old implementation from the Chrome binary. --headless=old no longer exists.
  • From 132 onward, the old mode survives only as a standalone binary called chrome-headless-shell, published on the Chrome for Testing dashboard.

Which one should a scraper run

chrome-headless-shell is smaller, starts faster, and uses less memory, because it carries no browser UI, no profile machinery, and no extension host. That last omission is the killer. Chrome's --proxy-server flag accepts no credentials, and the standard workaround is a small unpacked extension that answers the auth challenge. No extension host means no extension, which means no authenticated proxy without an external hop. The shell also fails more detection checks, for the reason above: it is not Chrome.

Practical rule: use chrome-headless-shell for pages you own, or for HTML you can fetch without facing defenses, where startup latency dominates. Use --headless (new mode) for anything that gets scored.

# New headless mode, real Chrome. What you want for scraping.
google-chrome --headless --disable-gpu --dump-dom https://www.sparkproxy.io/

# The old implementation, now a separate binary. Fast, but it is not Chrome.
chrome-headless-shell --dump-dom https://www.sparkproxy.io/
Free trial

Scraping at scale? Skip the blocks.

Fast, unblockable datacentre proxies with unlimited bandwidth.

The HeadlessChrome user agent tell, and what replaced it

Old headless Chrome advertised itself. The default User-Agent carried a HeadlessChrome/ product token instead of Chrome/, so a target could block automation with a substring match and no JavaScript at all. Free detection, and plenty of sites took it. Two things people get wrong about this in 2026.

First, the token did not disappear with the new mode. Chrome's unified headless still ships HeadlessChrome in the default User-Agent on common builds. If you switched to --headless=new and assumed the tell went away, check it:

const browser = await puppeteer.launch({ headless: true });
console.log(await browser.userAgent());
// Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko)
// HeadlessChrome/140.0.0.0 Safari/537.36

Override it explicitly, and override the matching Client Hints, or you end up with a UA that says one thing and a navigator.userAgentData that says another.

Second, headless Firefox never carried an equivalent token. Running Firefox with -headless or MOZ_HEADLESS=1, a mode Mozilla introduced back in Firefox 55/56, produces the same User-Agent as headful Firefox. Genuinely one fewer thing to patch.

It is also close to worthless on its own. Both browsers set navigator.webdriver to true under automation, and no modern defense stops at the UA string. The real surface is canvas, WebGL, audio, font metrics, timing, and protocol artifacts, covered in headless browser detection and what is browser fingerprinting. Treat the UA difference as a footnote, not a reason to switch engines.

Protocols: CDP, Juggler, and the WebDriver BiDi convergence

Here the two stacks are genuinely different, and this is where most comparison posts stop at "Chrome uses CDP."

Chrome

One protocol, the Chrome DevTools Protocol. Enormous surface: Network. for interception and response bodies, Fetch. for auth challenges, Emulation. for device and CPU throttling, Page. for lifecycle events. Anything DevTools can do, your script can do.

Firefox

Three overlapping histories, which is why Firefox automation feels inconsistent:

  1. Marionette, the classic WebDriver remote protocol that geckodriver fronts. Command and response, no events.
  2. An experimental CDP shim. Mozilla deprecated it starting with Firefox 129 in 2024, off by default, with removal targeted for the end of that year and Firefox 128 ESR carrying it about a year longer. Any tutorial telling you to drive Firefox over CDP is dead.
  3. WebDriver BiDi, the replacement, now a W3C Working Draft dated 29 June 2026. It is a WebSocket JSON protocol with real events, covering browsing contexts, network interception, sandboxed script evaluation, input, storage, logging, and web extension installation.

Where Playwright fits, and why it matters

Playwright uses none of the three. It ships a patched Firefox build carrying an XPCOM component called Juggler, because stock Firefox did not expose the control surface Playwright wanted. So "headless Firefox" under Playwright is not the Firefox on your machine, it is a Mozilla-derived build with Playwright's patches and preference overrides applied. For fingerprinting work that distinction is not academic: you are testing against a browser no real user runs.

Puppeteer took the other path. Per the Puppeteer BiDi documentation, Firefox is driven over WebDriver BiDi by default against stock Firefox, while Chrome still defaults to CDP because "not all CDP features are supported by WebDriver BiDi yet." Ask for something BiDi cannot do and Puppeteer throws UnsupportedOperation.

The current BiDi gaps that hurt scrapers specifically:

CapabilityCDP on ChromeBiDi on Firefox (Puppeteer)
Request interceptionyesyes
Response body captureyesyes
CPU throttlingyesnot supported
Network condition emulationyesnot supported
Media / vision emulationyesnot supported
Extension install and controlyesnot supported
Service worker interceptionyesnot supported
JS coverageyesnot supported

The convergence is real, and BiDi is the correct long-term bet for both engines. It is not finished. Write against your library's abstraction rather than raw CDP calls and the eventual migration costs a config line instead of a rewrite. If you are still choosing an automation library, Playwright vs Selenium for web scraping compares the layer above this one.

Memory and CPU at concurrency, the real cost driver

Single page load time is roughly a wash between the two engines, and it is the wrong metric anyway. The number that decides your bill is how many concurrent sessions fit on one machine before the box starts swapping. That is a process model question, not an engine speed question, and the two browsers take opposite positions on it.

Chrome multiplies processes

Chrome runs site isolation, which allocates a renderer process per site instance. A page with five cross-origin iframes, which is a normal ad-funded page, can be six renderers plus the browser process, plus GPU and network utility processes. Excellent security, expensive concurrency. Scale that to forty parallel contexts on targets with heavy third-party embeds and the process count runs into the hundreds.

Firefox caps processes by default

Firefox's documented process model is a pool. dom.ipc.processCount defaults to 8 shared web content processes. Under Fission, per-site isolated processes are governed by dom.ipc.processCount.webIsolated, and dom.ipc.processPrelaunch.fission.number preallocates 3 processes to hide launch latency. The pool is a ceiling by design.

That single default is the most underrated fact in this comparison. Under memory pressure, Firefox's failure mode is contention inside a bounded process pool. Chrome's failure mode is an unbounded process count and an OOM kill.

The levers you actually have

GoalChromeFirefox
Cap renderer processes`--renderer-process-limit=N``dom.ipc.processCount`
Cap per-site processes`--process-per-site``dom.ipc.processCount.webIsolated`
Disable process preallocationnot available`dom.ipc.processPrelaunch.enabled=false`
Avoid shared memory crashes in Docker`--disable-dev-shm-usage`not applicable
Cut GPU overhead on servers`--disable-gpu``gfx.webrender.software=true`

--disable-dev-shm-usage deserves its own line. Docker gives a container 64 MB of /dev/shm by default, Chrome writes shared memory there, and tabs start dying with Target closed under concurrency. Either pass that flag or run the container with --shm-size=1g. This is the single most common "my scraper falls over at scale" bug in headless Chrome, and headless Firefox does not have it.

Measure it yourself, do not trust published numbers

Per-instance memory depends entirely on your target pages, so any blog quoting "Chrome uses 380 MB per instance" is quoting their targets, not yours. Measure on your own URLs:

# Sum RSS across the whole browser process tree, in MB.
# Run this while your scraper holds N contexts open.
ps -eo rss,comm --no-headers \
  | grep -E 'chrome|firefox' \
  | awk '{sum += $1} END {printf "%.1f MB across %d procs\n", sum/1024, NR}'

Run it at 1, 5, 10, and 25 concurrent contexts against your real target list. The slope between those points is your capacity planning number. It is the only benchmark that means anything.

Market share cuts both ways for fingerprinting

Chrome sits around 65% of worldwide browser share and Firefox under 3%, per StatCounter. That ratio drives four consequences, and they do not all point the same way.

Anti-bot heuristics are tuned on Chrome. Vendors spend their detection budget where the traffic is. A large share of published checks target Chrome specifically: the shape of the window.chrome object, CDP side effects such as Runtime.enable altering error stack serialization, Blink-only API presence, and artifacts left behind by Chrome-focused patch libraries. On Gecko most of those simply do not apply. That is a real advantage, and it is why some teams keep a Firefox fallback for one stubborn target.

Rarity is itself a signal. If a target's traffic is 3% Firefox and your crawler sends 100% Firefox from one ASN, you are conspicuous at the aggregate layer even when every individual request looks internally consistent. Fingerprint consistency protects the request. It does not protect the pattern.

The stealth ecosystem is Chrome-shaped. The well-known patch plugins target Chrome tells, they age against vendors who ship weekly, and no maintained Firefox equivalent exists. On Firefox you are clean by default on Chrome-specific checks and on your own for everything else. Fair trade only if you were going to write your own patches anyway.

Your TLS fingerprint has to match your engine. Firefox uses NSS, Chrome uses BoringSSL. They produce different cipher orders and extension layouts, so they hash to different JA3 and JA4 values. Sending a Firefox User-Agent from a Chrome TLS stack, or the reverse, is a contradiction visible before a single byte of HTML is served. This one trips people who spoof the UA to look like the majority browser while running the minority engine. See what is TLS fingerprinting for how the hashes are built.

Per-context proxies, where the two diverge hardest

If you rotate IPs, this section decides your architecture. There are two shapes: one browser with N contexts on N proxies, which is cheap, and N browsers each on one proxy, which is not.

Chrome

--proxy-server is process-global and carries no credentials, so on its own it cannot give you per-session IPs. Playwright solves it at the context layer, credentials included:

const browser = await chromium.launch();

const ctx = await browser.newContext({
  proxy: {
    server: 'http://proxy.sparkproxy.io:8000',
    username: 'sp-user-session-a1',
    password: 'YOUR_PASSWORD',
  },
});
const page = await ctx.newPage();
await page.goto('https://www.sparkproxy.io/');

Spin up a second context with different credentials and it exits from a different IP, sharing one browser process tree. That is the cheap shape.

Firefox

Firefox has no native concept of a per-context proxy. Proxy settings are profile preferences (network.proxy.type, network.proxy.http, and friends), and a profile is global to the process. Everything above that is a Playwright patch.

It was also broken. Playwright issue #31525 documented Firefox contexts silently collapsing onto the most recently set proxy after the first navigation, so two contexts you believed were on two IPs were both on one. Reported against 1.41.2 through 1.44.1, fixed for 1.46. If any part of your fleet is pinned below 1.46 and running Firefox contexts, audit your exit IPs today.

Puppeteer plus Firefox over BiDi has no per-context proxy at all. One proxy per browser launch, which multiplies your process count by your IP count.

The fallback that always works

Point the browser at one local address and do rotation upstream, with a forward proxy or a rotating endpoint. It costs a hop and removes an entire class of browser-specific bugs. Manual configuration for both browsers is covered in how to configure a proxy in Chrome and Firefox.

Rendering engine differences that change what you extract

Both engines implement the HTML parsing spec, so for well-formed markup the DOM tree is the same and outerHTML matches. The rule worth memorizing: parse-tree extraction is portable, computed and rendered extraction is not.

Where Firefox will hand you different data:

  • Layout-dependent extraction. Bounding boxes, visibility checks, and element screenshots depend on font metrics. Blink and Gecko pick different default fonts and different fallback chains, and inside a slim Docker image with no fonts installed both degrade, differently. Any selector reasoning about position, and any screenshot pipeline feeding OCR, needs re-verification per engine.
  • Chrome-only web APIs. window.chrome is undefined on Firefox. Sites gating features behind Blink-only APIs render a reduced page, so you extract less with no error to catch.
  • Media. Firefox obtains H.264 through a runtime OpenH264 download that automation builds typically will not have. If your target embeds video and the player must initialize, that is a hard stop.
  • PDF output. Playwright's page.pdf() is Chromium-only.
  • Coverage and raw protocol access. The Coverage API and CDPSession are Chromium-only. Any escape hatch you built on raw CDP has no Firefox equivalent.

None of that touches static HTML extraction, which behaves identically on both. The divergence starts the moment your extraction depends on what the engine computed, which is exactly the case on the JavaScript-heavy targets covered in how to scrape dynamic JavaScript websites.

The decision rule

Work down this list and stop at the first match.

  1. You need per-context proxies inside one browser. Chrome. Firefox works only on Playwright 1.46 or newer, and not at all on Puppeteer.
  2. You need CDP-only capability: raw protocol access, extensions, CPU throttling, coverage, PDF output. Chrome. There is no Firefox path.
  3. Your target visibly scores Chrome-specific tells and you have already tried a real UA, matching Client Hints, and a residential exit. Try headless Firefox. Keep the deployment small, because rarity is its own signal.
  4. You are memory-bound on shared hardware and can accept a smaller feature surface. Firefox, using dom.ipc.processCount as a hard ceiling. Measure with the script above before committing.
  5. You are running a broad crawl across mixed targets. Chrome, new headless mode, one browser and N contexts, proxies at the context layer.
  6. Everything else. Chrome.

There is a seventh case, and it is the one most teams should take: if the browser exists only to get past defenses rather than to run real page logic, stop maintaining browsers.

Skip the browser entirely: the SparkProxy Scraping API

Everything above is a list of things you have to own: two process models, two protocol surfaces, two proxy stories, a stealth patch set that decays, and a fleet whose capacity you re-measure whenever a target changes its ad stack.

The SparkProxy Scraping API runs the headless browser on our side and hands back HTML, Markdown, or parsed fields. render_js drives the browser, wait_for blocks on a CSS selector, block_resources drops images and fonts, premium_proxy routes through residential IPs, country_code geo-targets, stealth applies the fingerprint work, and extract_rules returns fields instead of markup.

import requests

resp = requests.get(
    "https://scrape.sparkproxy.io/api/v1",
    headers={"X-API-Key": "YOUR_API_KEY"},
    params={
        "url": "https://www.sparkproxy.io/pricing",
        "render_js": "true",
        "wait_for": ".pricing-card",
        "block_resources": "true",
        "premium_proxy": "true",
        "country_code": "DE",
        "stealth": "true",
    },
    timeout=90,
)
print(resp.text)

Sessions pin an exit IP across calls, which is the managed equivalent of the per-context proxy problem above, without the version-pinning audit:

params = {
    "url": "https://www.sparkproxy.io/",
    "render_js": "true",
    "premium_proxy": "true",
    "session_id": "crawl-de-01",
    "format": "json",
    "extract_rules": '{"plan": ".pricing-card h3", "price": ".pricing-card .price"}',
}
data = requests.get(
    "https://scrape.sparkproxy.io/api/v1",
    headers={"X-API-Key": "YOUR_API_KEY"},
    params=params,
    timeout=90,
).json()

The trade is per-request cost against engineering time and server capacity. If you read this far because a fingerprint problem is eating your sprint, the crossover already happened.

Frequently asked questions

FAQ

Sometimes, for the narrow reason that most published detection checks target Chrome and simply do not apply to Gecko. Firefox is not inherently stealthier: it still sets navigator.webdriver, it still exposes automation artifacts, and its sub-3% share means a Firefox-only fleet is conspicuous in aggregate traffic analysis even when each session looks clean.

Yes, on common builds the default User-Agent still contains a HeadlessChrome/ product token even under --headless=new. Override the User-Agent and the matching navigator.userAgentData Client Hints together, because a mismatch between those two is a stronger signal than the original token ever was.

Firefox, usually, because its process pool is bounded by dom.ipc.processCount (default 8) while Chrome's site isolation allocates a renderer per site instance with no natural ceiling. The size of the gap depends entirely on how many cross-origin frames your targets embed, so measure the RSS of the whole process tree at 1, 5, 10, and 25 contexts against your own URL list.

Only under Playwright 1.46 or newer. Firefox itself has no per-context proxy concept, since proxy settings are profile preferences, and Playwright issue #31525 documented earlier versions collapsing every context onto the most recently set proxy after the first navigation. Puppeteer driving Firefox over WebDriver BiDi has no per-context proxy at all.

Only for rendering work that faces no defenses, such as internal screenshots or PDF pipelines, where startup latency matters more than realism. It is the old headless implementation, it is not Chrome, it fails client-side scoring more often, and it has no extension host, which rules out the extension-based workaround for authenticated proxies.

For Firefox that decision is already made: Mozilla deprecated its CDP shim starting with Firefox 129, so BiDi is the supported path. Expect gaps against CDP, including no CPU throttling, no network condition emulation, no extension control, and no coverage, and write against your automation library's abstraction rather than raw protocol calls so the remaining gaps close without a rewrite.

Limited-time ยท 50% off

Get 50% off your first month

Premium datacentre proxies with unlimited bandwidth. Use the code at checkout.

Offer ends soon โ€” claim it before it's gone

Claim Discount

About the Author

The SparkProxy Technical Team builds and operates SparkProxy's datacenter proxies, residential proxies, and Scraping API. We run headless browser fleets in production against live anti-bot systems, which is where the process model, protocol, and proxy details in this post come from. Documentation for every parameter shown here lives in the SparkProxy Scraping API docs.

Keep reading

Related articles

cURL vs Python Requests for Web Scraping (2026)

cURL vs Python Requests for Web Scraping (2026)

curl vs Python Requests for web scraping: how TLS fingerprinting, HTTP/2, connection pooling, proxy syntax, and streaming differ, and which to use when.

SparkProxyยทComparisons
Antidetect Browser vs Proxies: Which Do You Need?

Antidetect Browser vs Proxies: Which Do You Need?

Antidetect browser vs proxies: a decision rule based on what your target actually keys on, the three mismatch failure modes, and a checklist that picks for you.

SparkProxyยทComparisons