Stealth Browser Benchmark: Patchright, nodriver, Camoufox (2026)

54 runs, five automation tools, two IP types. Browser-level stealth decided every bot verdict; the residential IP only moved network-side checks, and not always for the better.

Stealth Browser Benchmark: Patchright, nodriver, Camoufox (2026)
In this article

We ran a small stealth browser benchmark on 3 October 2026: plain Playwright as the control, then Patchright, nodriver, Camoufox and Zendriver, all against public bot-detection test pages, once from a Hetzner datacenter IP and once through a US residential proxy. The browser decided every automation verdict. Plain Playwright was flagged by deviceandbrowserinfo, BrowserScan, Fingerprint’s playground and Pixelscan in every run from both IPs. Run headful, the four stealth tools passed the first three in every run that produced a verdict, and Pixelscan flagged 2 of their 39 headful runs (2 more were n/a). The residential IP only moved the checks that look at the network, and not always in the right direction.

That's 54 recorded runs from one host and one proxy country, on pages built for testing, not a ranking of anyone's production anti-bot. Below: the method, the raw verdicts, what the data does not support, and the code to rerun it.

Stealth browser benchmark: the short answer

If the browser leaks automation, a residential IP doesn't save it. If the browser is clean, the IP mostly affects network-side checks: proxy detection, timezone consistency and WebRTC. On these pages that cut both ways:

  • Automation verdicts didn't change with the IP. Playwright got isBot: true on deviceandbrowserinfo, “WebDriver: Robot” on BrowserScan and “bad bot (type: webdriver)” on Fingerprint’s playground in all 10 of its headful runs, from both networks. Patchright, nodriver, Zendriver and Camoufox got none of those flags in any headful run.
  • Pixelscan cared about the datacenter IP. It printed “Proxy detected” in 20 of 20 runs from the Hetzner IP, even though those runs used no proxy at all. Through the residential proxy it printed that in 3 of 34 runs.
  • Fingerprint’s playground cared about the residential IP. Its proxy signal flagged 33 of 34 residential runs, often naming a proxy network (“residential proxy from SOAX”, “from INFATICA”, and so on). It didn't flag the Hetzner IP for the Chrome-based tools. For the three Chrome-based stealth tools, suspect scores were 0–6 from the datacenter and 8–24 through the sticky residential proxy.
  • Three of the five tools exposed the server’s real IP over WebRTC through the proxy. Playwright, Patchright and nodriver did in every proxied run. Zendriver blocks WebRTC by default and Camoufox spoofs it.
  • Headless mode mattered more than the IP. In headless mode, Patchright and nodriver got flagged on the same pages they passed headful.

What we ran: versions, network conditions, targets

Everything ran in one Debian 12 container with Xvfb, one browser at a time, on a 4-core Linux host. Each tool used the launch config its own docs recommend and nothing more: no custom user agent, no fingerprint injection, no mouse movement, no resource blocking. For Patchright that's the README’s “Best Practice” config: a persistent context, real Chrome, headful, no_viewport=True.

ToolPackageBrowserLaunch
Playwright (control)playwright 1.62.0Playwright Chromium 151.0.7922.34chromium.launch(headless=False), defaults
Patchrightpatchright 1.63.0Google Chrome 154.0.8037.97launch_persistent_context(channel="chrome", headless=False, no_viewport=True)
nodrivernodriver 0.50.3Google Chrome 154.0.8037.97start(headless=False, sandbox=False)
Zendriverzendriver 0.17.1Google Chrome 154.0.8037.97start(headless=False, sandbox=False)
Camoufoxcamoufox 0.5.6Camoufox 152.0.4-beta.31AsyncCamoufox(headless=False, os="linux", geoip=True)

Two compromises, stated up front. The control is Playwright 1.62.0, not the current 1.63.0, because camoufox 0.5.6 requires playwright<1.63 and all tools shared one environment. And Camoufox ran with os="linux" and only its Linux fonts, because the host disk was nearly full and we skipped extracting the macOS and Windows font bundles. Camoufox’s default would pick a random OS.

Network conditions:

  • Datacenter: the host’s own IP, AS24940 Hetzner Online GmbH, Helsinki (per ipinfo.io).
  • Residential, sticky: a US residential exit with a fresh sticky session per run. In every proxied run, all tools connected through the same small local relay. It adds proxy auth and counts bytes, because nodriver and Zendriver can't pass proxy credentials in a Chrome flag.
  • Three side conditions: a rotating residential pass with a new exit IP on every connection, a warm-up pass (explained below), and one headless run per tool from the datacenter IP.

Targets, visited in this order in a fresh browser per run, each in a new tab: bot.sannysoft.com, BrowserScan bot detection, deviceandbrowserinfo “Are you a bot?”, CreepJS, Fingerprint’s playground, nowsecure.nl (behind Cloudflare; plain curl gets a 403 with cf-mitigated: challenge), and Pixelscan’s bot check and fingerprint check. Fingerprint’s bot-detection product page was dropped because it only shows a static JSON sample, not a live verdict.

Measurements: whether the page loaded, the verdict exactly as the page printed it, and the time until that verdict appeared (polled every 0.5 s, 45 s timeout). We saved the page text and a full-page screenshot for every visit. A verdict that couldn't be parsed is recorded as “unparsed” and never guessed. Four headful visits ended that way, three page timeouts and one mid-update capture. They're counted as n/a in the tables.

Runs: 3 per tool per condition for datacenter, rotating and sticky residential, 1 per tool for headless and warm-up. 54 runs in total, between 06:50 and 08:02 UTC.

Results: browser-level automation checks

The table counts runs: “3/3” means the page flagged the browser in all three. Every cell comes from the parsed results file.

ToolNetworksannysoft failed rowsBrowserScan flaggeddeviceandbrowserinfo isBotCreepJS headlessFingerprint “Bot”Pixelscan bot banner
Playwright (control)Datacenter (Hetzner)WebDriver3/33/333%3/3: You are a bad bot (type: webdriver)3/3
Playwright (control)Residential US, stickyWebDriver3/33/333%3/3: You are a bad bot (type: webdriver)3/3
PatchrightDatacenter (Hetzner)none0/30/30%Not detected (3/3)0/3
PatchrightResidential US, stickynone0/30/30%Not detected (3/3)0/3
nodriverDatacenter (Hetzner)none0/30/30%Not detected (3/3)0/3
nodriverResidential US, stickynone0/30/30%Not detected (3/3)0/2 (+1 n/a)
ZendriverDatacenter (Hetzner)none0/30/30%Not detected (3/3)0/3
ZendriverResidential US, stickynone0/30/30%Not detected (3/3)0/3
CamoufoxDatacenter (Hetzner)Chrome0/30/30%Not detected (3/3)0/3
CamoufoxResidential US, stickyChrome0/30/30%Not detected (3/3)0/3

A few details the table hides:

  • sannysoft’s only failed row for Camoufox is “Chrome (New): missing”. That's expected for a Firefox-based browser, not an automation leak.
  • CreepJS also prints a “% like headless” score. It was 50% for Playwright, 44% for the three Chrome-based stealth tools and 6% for Camoufox, in every headful run. Its “% stealth” score was 0% for everything. CreepJS doesn't label any of these numbers pass or fail.
  • Pixelscan’s bot check flipped twice. Patchright got “Bot Behavior Detected” (CDP tile) in the single warm-up run, and Zendriver once in the rotating pass. Both are visible in the screenshots. Two more Pixelscan visits are n/a. One timed out. In the other, our text capture caught the page mid-update, and the screenshot taken a moment later shows “You're Definitely a Human”.
  • nowsecure.nl showed its page content in every headful run, with no challenge left on screen, plain Playwright from the datacenter IP included. Headful, it didn't separate the tools at all.

For why these particular checks catch stock Playwright (the navigator.webdriver flag, CDP Runtime.enable side effects, launch flags), see our Patchright deep-dive and browser fingerprinting explainer.

Results: IP-aware signals, datacenter vs residential

Only two of the pages score the network. Fingerprint’s playground shows VPN, proxy, IP blocklist and a suspect score. Pixelscan’s fingerprint check shows proxy and location consistency. deviceandbrowserinfo says outright that its test “does not use IP reputation or user behavior”. Its verdicts not moving with the IP is consistent with that.

ToolNetworkFingerprint proxy flagFingerprint VPN flagFingerprint suspect scorePixelscan “Proxy detected”Pixelscan location checkWebRTC leaks the server IP behind the proxy
Playwright (control)Datacenter (Hetzner)0/30/3193/3Timezone spoofedn/a (no proxy; real IP is expected)
Playwright (control)Residential US, sticky2/30/325–270/3Timezone spoofed3/3
PatchrightDatacenter (Hetzner)0/30/30–63/3Timezone spoofedn/a (no proxy; real IP is expected)
PatchrightResidential US, sticky3/30/38–200/3Timezone spoofed3/3
nodriverDatacenter (Hetzner)0/30/30–63/3Timezone spoofedn/a (no proxy; real IP is expected)
nodriverResidential US, sticky3/32/310–230/3Timezone spoofed3/3
ZendriverDatacenter (Hetzner)0/30/30–63/3Timezone spoofedblocked (3/3)
ZendriverResidential US, sticky3/30/38–240/3Timezone spoofedblocked (3/3)
CamoufoxDatacenter (Hetzner)3/33/3173/3Timezone spoofedn/a (no proxy; real IP is expected)
CamoufoxResidential US, sticky3/30/34–141/3matched (US city shown)0/3

Where the residential IP helped

Pixelscan stopped printing “Proxy detected”. It printed it in every datacenter run, even though no proxy was involved, which suggests it reads the hosting ASN as a proxy. Through the residential pool it printed it in 1 of 15 sticky runs. Camoufox also got a matching location. Its geoip=True option sets timezone, locale, geolocation and the WebRTC IP from the exit IP, so Pixelscan showed a US city instead of “Timezone spoofed”.

Where it hurt or did nothing

Fingerprint’s proxy signal recognised the residential exits as proxy IPs in 33 of 34 proxied runs, across all three proxy modes, and attached provider names to many of them. We read that as Fingerprint having intelligence on residential pools. It doesn't mean every IP leaked something, and it says nothing about what a given site would do with that signal. For the Chrome-based stealth tools the suspect score went up through the proxy, not down. Playwright’s went from 19 to 25–27, and the one warm-up run scored 47.

Part of that is likely self-inflicted. The Chrome-based tools kept the container’s UTC timezone, so a US IP with a UTC clock read as “Timezone spoofed” on Pixelscan in every proxied run. Camoufox shows the same mechanism in the other direction. From the Helsinki datacenter IP, its GeoIP lookup placed the exit in Europe/Berlin while ipinfo said Helsinki. Fingerprint then flagged a VPN (“timezone mismatch”) and a low-confidence datacenter proxy in all three of its datacenter runs, while the Chrome tools on the same IP got neither flag. An IP and a timezone that disagree get noticed either way.

WebRTC leaked the server behind the proxy

CreepJS and Pixelscan both printed the host’s Hetzner address as the WebRTC IP in every proxied Playwright, Patchright and nodriver run. The HTTP traffic went through the residential exit, but the STUN request over UDP didn't. Zendriver passes --webrtc-ip-handling-policy=disable_non_proxied_udp and --force-webrtc-ip-handling-policy by default, and CreepJS reported WebRTC as blocked in all of its runs. Camoufox replaced the address with the proxy exit. A blocked WebRTC is itself a signal some sites may score. We didn't test whether any of these pages penalise it, and Pixelscan didn't. See how anti-bot systems detect proxies for the other mismatches they look for.

Headless mode was a bigger tell than the IP

We ran one headless run per tool from the datacenter IP. This sample is smaller than the rest, but the direction is clear:

Tool (headless, datacenter IP, 1 run)sannysoft failed rowsdeviceandbrowserinfo isBotCreepJS headlessFingerprint “Bot”nowsecure.nl
Playwright (control)CHR_MEMORY:FAIL, HEADCHR_UA:FAIL, WebDriver1/1100%1/1: You are a bad bot (type: headless_chrome)stuck on “Just a moment...” after 45 s
PatchrightCHR_MEMORY:FAIL, HEADCHR_UA:FAIL1/167%1/1: You are a bad bot (type: headless_chrome)stuck on “Just a moment...” after 45 s
nodriverCHR_MEMORY:FAIL, HEADCHR_UA:FAIL1/167%1/1: You are a bad bot (type: headless_chrome)stuck on “Just a moment...” after 45 s
Zendrivernone1/133%Not detected (1/1)stuck on “Just a moment...” after 45 s
CamoufoxChrome0/10%Not detected (1/1)page loaded

Patchright and nodriver went from clean to flagged on deviceandbrowserinfo and Fingerprint (“bad bot, type: headless_chrome”) just by switching to headless. sannysoft failed its HEADCHR_UA and CHR_MEMORY rows. Every Chrome-based tool, Zendriver included, stalled on Cloudflare’s “Just a moment...” page at nowsecure.nl. Headful, the same tools, plain Playwright included, got straight through. Zendriver avoided Fingerprint’s bot flag but was still flagged by deviceandbrowserinfo. Camoufox looked the same headless as headful on all of these pages.

Two operational findings: sticky sessions and background traffic

Parallel first connections broke sticky sessions

With a sticky session ID, Chrome-based runs often showed one IP on the first page and a different one a few pages later. That happened in 11 of 12 sticky runs. Camoufox stayed on one IP in 3 of 3, probably because its GeoIP lookup makes a single request before the browser starts. We reproduced it with curl: six parallel requests on a brand-new session ID returned 3 and 5 distinct IPs in two trials. Sequential requests afterwards stayed on one. Adding one serial warm-up request before launching the browser fixed it in 4 of 4 runs. Our first proxy pass also used a session parameter the gateway doesn't recognise, so every connection silently got a new IP. That pass is reported above as the rotating condition. The lesson from both: check stickiness with two sequential requests before trusting it.

Some real-Chrome setups download in the background through your proxy

On metered residential traffic, Playwright used about 7 MB per run of these 9 pages, Patchright about 8 MB (12 MB in one run) and Camoufox 9–11 MB. Most nodriver and Zendriver runs used 40–61 MB. In the runs where we logged bytes per host, most of that came from optimizationguide-pa.googleapis.com, a Chrome background download. Two runs used only about 9 and 15 MB, so it doesn't happen every time. Patchright also runs real Chrome on a fresh profile but didn't show this, so it depends on how the tool launches Chrome. If you pay per GB, count bytes per host before you scale.

What this benchmark does not show

  • It isn't a production anti-bot test. These pages mostly check browser-side artefacts. Commercial systems add TLS and HTTP/2 fingerprints, behaviour, IP history and per-site models. A clean result here is a necessary bar, not a sufficient one. Our TLS fingerprinting guide covers one layer these pages barely touch.
  • It's a small sample: 3 runs per cell, one day, one datacenter host in Finland, one proxy country. Single flips, like Pixelscan’s, can't be separated from noise.
  • There was no interaction. No clicks, scrolling, forms or mouse paths, so behavioural detection wasn't exercised.
  • Some checks keep history. Fingerprint’s playground shows per-device history (“1 IP, 2 Linked IDs in the past 24 hours”), so repeat visits may have affected later scores.
  • Defaults were not tuned. The Chrome-based tools kept UTC and en-US, Camoufox was limited to Linux, and nothing was hardened beyond each README’s default. A tuned setup might score differently. That's what we'd test next, not something we measured.
  • Your targets will differ. Whether to use a datacenter or residential IP depends on what you collect and where. Residential vs datacenter for scraping covers the cost side. Check robots.txt, the site’s terms and personal-data rules before running anything like this against a real site.

Reproduce it

The harness is about 260 lines of Python, an 80-line relay and a parser that only reads saved page text. Below is the Patchright setup that ran clean in our data, with the two fixes from above built in: a warm-up request that pins the sticky session before Chrome opens parallel connections, and Zendriver’s WebRTC flags. We ran it once against deviceandbrowserinfo through a US residential exit ("isBot": false); that check isn't part of the 54 runs.

import asyncio, os, tempfile, urllib.parse, urllib.request, uuid
from patchright.async_api import async_playwright

USER = os.environ["PROXY_USER"]          # your proxy username
PASS = os.environ["PROXY_PASS"]          # your proxy password, never hard-code it
GATE = "gate.proxyhat.com:8080"

def sticky_username() -> str:
    # one fresh session per browser launch; -sid- keeps the exit IP between connections
    return f"{USER}-country-US-sid-{uuid.uuid4().hex[:10]}-ttl-30m"

def warm_up(username: str) -> str:
    # one serial request so the session is pinned before Chrome opens parallel connections
    url = f"http://{urllib.parse.quote(username)}:{urllib.parse.quote(PASS)}@{GATE}"
    opener = urllib.request.build_opener(urllib.request.ProxyHandler({"http": url, "https": url}))
    return opener.open("https://api.ipify.org", timeout=30).read().decode()

async def main():
    username = sticky_username()
    print("session pinned to", warm_up(username))
    async with async_playwright() as p:
        ctx = await p.chromium.launch_persistent_context(
            user_data_dir=tempfile.mkdtemp(),
            channel="chrome",            # real Google Chrome, as the Patchright README recommends
            headless=False,              # run under Xvfb on a server
            no_viewport=True,
            proxy={"server": f"http://{GATE}", "username": username, "password": PASS},
            args=["--webrtc-ip-handling-policy=disable_non_proxied_udp",
                  "--force-webrtc-ip-handling-policy"],
        )
        page = await ctx.new_page()
        await page.goto("https://deviceandbrowserinfo.com/are_you_a_bot")
        await page.wait_for_function("document.body.innerText.includes('\"isBot\"')", timeout=45000)
        print(await page.evaluate("document.body.innerText.match(/\"isBot\": (true|false)/)[0]"))
        await ctx.close()

asyncio.run(main())

The username suffix is ProxyHat syntax (-country-US for targeting, -sid-…-ttl-30m for a sticky session). Swap in your provider’s format. To check an exit before a long run, the free proxy checker shows what a site sees. To score a cell the way we did:

  1. Start a fresh browser per run, visit each page in a new tab, and wait until the page’s own verdict text is on screen. Don't use a fixed sleep. We gave CreepJS 6 extra seconds after its scores appeared, because it keeps computing.
  2. Save document.body.innerText and a full-page screenshot before closing the tab.
  3. Parse only what the page printed, for example the "isBot" JSON on deviceandbrowserinfo, the Normal/Robot pairs on BrowserScan, and “N% headless” on CreepJS. Anything else is “unparsed”.
  4. Record the exit IP at the start and compare it with the IP the last page shows. If they differ, your session wasn't sticky.
  5. Count proxy bytes per run, so background traffic shows up before the invoice does.

What to do with this

Fix the browser first: stock Playwright was caught from both IPs, the four stealth tools weren't caught headful from either. Run headful (Xvfb on servers) unless you've measured that headless passes your targets. Then make the network story consistent: match the timezone to the exit IP, block or proxy WebRTC, and confirm your sticky session really is sticky. Add a residential proxy only after that, and expect proxy-aware vendors to recognise many residential IPs anyway. The IP decides if you start out labelled as datacenter traffic. The browser decides if you get flagged as a bot.

Frequently asked questions

Does a residential proxy stop Playwright from being detected as a bot?

Not on the pages we tested. Stock Playwright was flagged by deviceandbrowserinfo, BrowserScan and Fingerprint's playground in all 10 of its headful runs, from a datacenter IP and through a US residential proxy. The flags came from browser-side signals like navigator.webdriver, which the IP doesn't change.

Is Patchright, nodriver or Camoufox better at passing bot detection tests?

Headful, all three, plus Zendriver, passed every automation check on sannysoft, BrowserScan, deviceandbrowserinfo, Fingerprint's playground and Pixelscan in almost every run. Pixelscan flagged Patchright once and Zendriver once. In one headless run each, Patchright and nodriver got flagged by several pages, Zendriver by deviceandbrowserinfo, and Camoufox by none. The differences that showed up were in WebRTC handling and timezone, not in automation verdicts.

Why does my browser show my server's IP even though I use a proxy?

WebRTC can send a STUN request over UDP outside the HTTP proxy, which exposes the machine's real public IP. In our runs Playwright, Patchright and nodriver exposed the host IP this way in every proxied run. Zendriver's default Chrome flags block non-proxied UDP, and Camoufox replaced the address with the proxy exit.

Why does my sticky proxy session change IP when I open a browser?

In our tests, several parallel first connections on a brand-new session ID came out of different exit IPs before the session settled on one. Chrome opens many connections at startup, so the first page and later pages saw different IPs in 11 of 12 runs. Sending one serial request on the new session before launching the browser kept a single IP in 4 of 4 runs.

Test your proxies against real anti-bot defenses

Free proxy checker — latency, anonymity and block signals in one click.

Run a free check
← Back to Blog