Every "best stealth browser 2026" list I can find has the same problem. The scores come from nowhere, half the recommended tools have not shipped a commit in over a year, and nobody publishes the code they ran. So I installed all of them and ran the tests myself.

Seven automation tools, six public fingerprinting pages, headless and headed, three repetitions each. That is 42 runs, plus a human baseline from my own browser so the numbers mean something. Every result below came off my machine, and the harness is at the end so you can rerun it.

The short version: headed or headless matters more than which tool you pick, four tools are indistinguishable from a real browser when headed, and every Chromium-based tool leaks in headless mode - including the three that look clean, which turn out to be leaking something subtler than the one they fixed.

The Quick Answer

Tool Headed Headless Use it when
Camoufox Clean Clean You need headless and cannot run a display
SeleniumBase (CDP Mode) Clean Leaks Client Hints You want Python + a mature test framework
Botasaurus Clean Leaks Client Hints You want a scraping framework, not just a driver
Zendriver Clean Leaks Client Hints You liked nodriver and want it maintained
Nodriver Clean Leaks HeadlessChrome Legacy projects. See the dead-code bug below
Patchright Near-clean Leaks HeadlessChrome You have existing Playwright code
Playwright (control) Detected Detected Testing your own site, not scraping
HEADED sannysoft CreepJS are_you_a_bot BrowserScan fingerprint-scan Playwright Playwright - sannysoft: 1 check fails: WebDriver (New) Playwright - CreepJS: 33% headless Playwright - are_you_a_bot: bot: webdriver + CDP Playwright - BrowserScan: Robot Playwright - fingerprint-scan: score 100 Patchright Patchright - sannysoft: 0 failures Patchright - CreepJS: 0% headless Patchright - are_you_a_bot: human Patchright - BrowserScan: Normal Patchright - fingerprint-scan: score 15 Camoufox Camoufox - sannysoft: only window.chrome, which real Firefox also lacks Camoufox - CreepJS: 0% headless Camoufox - are_you_a_bot: human Camoufox - BrowserScan: Normal Camoufox - fingerprint-scan: score 5 SeleniumBase SeleniumBase - sannysoft: 0 failures SeleniumBase - CreepJS: 19/0/0, same as real Chrome SeleniumBase - are_you_a_bot: human SeleniumBase - BrowserScan: Normal SeleniumBase - fingerprint-scan: score 0-15 Botasaurus Botasaurus - sannysoft: 0 failures Botasaurus - CreepJS: 19/0/0, same as real Chrome Botasaurus - are_you_a_bot: human Botasaurus - BrowserScan: Normal Botasaurus - fingerprint-scan: score 0 Zendriver Zendriver - sannysoft: 0 failures Zendriver - CreepJS: 19/0/0, same as real Chrome Zendriver - are_you_a_bot: human Zendriver - BrowserScan: Normal Zendriver - fingerprint-scan: score 0-15 Nodriver Nodriver - sannysoft: 0 failures Nodriver - CreepJS: 19/0/0, same as real Chrome Nodriver - are_you_a_bot: human Nodriver - BrowserScan: Normal Nodriver - fingerprint-scan: score 0 HEADLESS sannysoft CreepJS are_you_a_bot BrowserScan fingerprint-scan Playwright Playwright - sannysoft: 4 failures Playwright - CreepJS: 100% headless Playwright - are_you_a_bot: bot: 6 signals Playwright - BrowserScan: Robot Playwright - fingerprint-scan: score 100 Patchright Patchright - sannysoft: 3 failures Patchright - CreepJS: 67% headless Patchright - are_you_a_bot: bot: HeadlessChrome UA Patchright - BrowserScan: Robot Patchright - fingerprint-scan: score 100 Camoufox Camoufox - sannysoft: only window.chrome, which real Firefox also lacks Camoufox - CreepJS: 0% headless Camoufox - are_you_a_bot: human Camoufox - BrowserScan: Normal Camoufox - fingerprint-scan: score 5 SeleniumBase SeleniumBase - sannysoft: 0 failures SeleniumBase - CreepJS: 33% headless SeleniumBase - are_you_a_bot: human SeleniumBase - BrowserScan: Normal SeleniumBase - fingerprint-scan: score 100 Botasaurus Botasaurus - sannysoft: 0 failures Botasaurus - CreepJS: 33% headless Botasaurus - are_you_a_bot: human Botasaurus - BrowserScan: Normal Botasaurus - fingerprint-scan: score 100 Zendriver Zendriver - sannysoft: 0 failures Zendriver - CreepJS: 33% headless Zendriver - are_you_a_bot: human Zendriver - BrowserScan: Normal Zendriver - fingerprint-scan: score 100 Nodriver Nodriver - sannysoft: 3 failures Nodriver - CreepJS: 67% headless Nodriver - are_you_a_bot: bot: HeadlessChrome UA Nodriver - BrowserScan: Robot Nodriver - fingerprint-scan: score 100 + UA & screen flags + UA & screen flags - sannysoft: 0 failures + UA & screen flags - CreepJS: 19/0/0, same as real Chrome + UA & screen flags - are_you_a_bot: human + UA & screen flags - BrowserScan: Normal + UA & screen flags - fingerprint-scan: score 15 ✓ matches a real browser ✗ flagged · median of 3 runs · hover a cell for the detail
Every tool against every page, both modes. Headed is almost entirely clean; headless is where they separate. The last row is stock Zendriver plus the two launch flags described later in this post.

The Contenders, and the Ones That Are Dead

Half the tools recommended in search results are abandoned. I checked every repository's last push on the day I ran these tests:

Project Approach Last push Status
SeleniumBase Patched chromedriver (UC Mode) and a pure CDP driver (CDP Mode) 2026-09-17 Active
Camoufox Firefox forked and patched at the C++ level 2026-09-14 Active
Patchright Patched Playwright, drop-in replacement 2026-09-13 Active
Zendriver Community fork of nodriver 2026-08-16 Active
Botasaurus Scraping framework with its own stealth driver 2026-07-26 Active
Nodriver Raw CDP to unmodified Chrome 2026-05-13 Slowing
undetected-chromedriver Patched chromedriver 2025-07-05 Stale
rebrowser-patches Puppeteer/Playwright runtime patches 2025-05-09 Stale
puppeteer-extra-stealth JS evasion plugins 2024-07-18 Dead

If an article published this year recommends puppeteer-extra-stealth, it is recommending a library that has not been touched in over two years while Chrome shipped roughly twenty major versions. Detection moved. It did not.

The three live approaches are worth understanding, because they explain the results.

Patchright patches Playwright's own leaks. The big one is Runtime.enable: standard CDP automation enables the Runtime domain to evaluate JavaScript, and that is directly observable from the page. Patchright runs JS in isolated execution contexts instead, disables the Console API entirely, and strips the giveaway launch flags (--enable-automation out, --disable-blink-features=AutomationControlled in).

Camoufox takes the hardest path: it forks Firefox and injects fingerprint data at the C++ implementation level, so there is no JavaScript patch for a detection script to find. Properties you do not set are filled in from BrowserForge, which samples real-world device distributions.

Nodriver, Zendriver, Botasaurus and SeleniumBase CDP Mode (the HTTP-client side of this stack is covered in Web Scraping Tools in 2026) all share one idea: never attach WebDriver at all. They speak raw CDP to a normal Chrome. There is no navigator.webdriver to hide because nothing set it.

Methodology

Windows 11, i5-13420H, 24 GB RAM, residential connection. Chrome 152.0.7977.84. Package versions: camoufox 0.5.6, patchright 1.62.3, playwright 1.62.0, seleniumbase 4.54.8, botasaurus-driver 4.0.101, zendriver 0.16.0, nodriver 0.50.3.

Each tool ran in its documented stealth configuration, in a fresh temporary profile, in its own subprocess, three times per mode. The pages:

Page What it reports
bot.sannysoft.com 31 individual checks, pass/fail
CreepJS Percentages for "like headless", "headless", "stealth"
deviceandbrowserinfo.com/are_you_a_bot A bot/human verdict plus the specific signals that fired
browserscan.net/bot-detection Overall verdict
fingerprint-scan.com A bot risk score from 0 to 100
tls.peet.ws JA4 TLS fingerprint and the wire headers

The baseline matters more than any score. A number like "19% like headless" is meaningless without knowing what a real browser scores. On my normal Chrome, driven by hand: CreepJS 19% like headless, 0% headless, 0% stealth, fingerprint-scan 0/100, deviceandbrowserinfo human. That is the target. Nothing can beat it, and matching it is the goal.

What I left out, and why. I did not test against Cloudflare Turnstile, and neither should any benchmark using the pages people usually reach for. The two "Turnstile test" pages that show up in every guide both use Cloudflare's dummy sitekeys: SeleniumBase's demo uses 3x00000000000000000000FF, which forces a challenge for everyone, and Cloudflare's own demo uses 1x00000000000000000000AA, which passes for everyone. Neither measures your browser. Testing against a real Turnstile key means testing against somebody's live protection, which is a different activity from benchmarking. So: fingerprinting pages only.

I also did not publish raw page output. These pages report your public IP through WebRTC and your timezone. Only scores left the machine.

The Control: What Vanilla Playwright Leaks

Before judging stealth tools, it helps to see what they are fixing. Vanilla Playwright driving real Chrome, headless:

  • sannysoft: 4 failures - User Agent (Old), WebDriver (New), HEADCHR_UA, CHR_MEMORY
  • CreepJS: 38% like headless, 100% headless, 0% stealth
  • deviceandbrowserinfo: bot, with six signals firing including hasWebdriverTrue, isAutomatedWithCDP and isAutomatedWithCDPInWebWorker
  • BrowserScan: Robot
  • fingerprint-scan: 100/100

Headed, it still fails. navigator.webdriver is still true, CDP is still detectable, and both isAutomatedWithCDP and isAutomatedWithCDPInWebWorker still fire. This is the floor everything else is measured against.

Results: Headed

All three repetitions agreed unless noted.

Tool sannysoft fails CreepJS (like/headless/stealth) are_you_a_bot BrowserScan fingerprint-scan
Real Chrome (baseline) n/a 19 / 0 / 0 human n/a 0
Playwright 1 31 / 33 / 0 bot (5 signals) Robot 100
Patchright 0 25 / 0 / 0 human Normal 15
Camoufox 1 (false positive) 6 / 0 / 0 human Normal 5
SeleniumBase 0 19 / 0 / 0 human Normal 15, 15, 0
Botasaurus 0 19 / 0 / 0 human Normal 0
Zendriver 0 19 / 0 / 0 human Normal 15, 0, 15
Nodriver 0 19 / 0 / 0 human Normal 0

Four tools hit 19/0/0, which is my real browser's score exactly. Botasaurus and Nodriver also scored 0 on fingerprint-scan, matching the baseline on every metric I measured.

Two notes on reading this table. Camoufox's single sannysoft failure is Chrome (New), which tests for window.chrome. Firefox does not have window.chrome, and a real Firefox fails it too, so that is a Chromium-specific check, not a detection. And the fingerprint-scan column shows why single-run benchmarks mislead: SeleniumBase and Zendriver scored 15 in some runs and 0 in others with no config change. Only Patchright scored 15 consistently. I could not pin down the cause, so I am reporting the variance rather than inventing an explanation.

Results: Headless

This is where they separate.

Tool sannysoft fails CreepJS are_you_a_bot BrowserScan fingerprint-scan
Playwright 4 38 / 100 / 0 bot (6 signals) Robot 100
Patchright 3 31 / 67 / 0 bot (hasBotUserAgent) Robot 100
Nodriver 3 25 / 67 / 0 bot (hasBotUserAgent) Robot 100
Camoufox 1 (false positive) 6 / 0 / 0 human Normal 5 (one run: 20)
SeleniumBase 0 31 / 33 / 0 human Normal 100
Botasaurus 0 31 / 33 / 0 human Normal 100
Zendriver 0 31 / 33 / 0 human Normal 100

Camoufox is the only tool that passes headless. Everything else is caught by at least one page.

Patchright and Nodriver leak the obvious thing: HeadlessChrome sits right in the user agent string, so hasBotUserAgent fires and BrowserScan says Robot.

The interesting group is SeleniumBase, Botasaurus and Zendriver. They pass sannysoft with zero failures, pass deviceandbrowserinfo as human, pass BrowserScan, and still score 100/100 on fingerprint-scan and 33% headless on CreepJS. Something is giving them away that four other pages miss.

The Nodriver Bug: A Fix That Never Runs

Nodriver 0.50.3 contains the code to fix its own UA leak. In nodriver/core/tab.py there is a _prepare_headless() method that reads navigator.userAgent and re-sends it with "Headless" stripped out. It works. It is just never called.

pip install nodriver==0.50.3
grep -rn "_prepare_headless" "$(python -c 'import nodriver, os; print(os.path.dirname(nodriver.__file__))')" --include=*.py
# nodriver/core/tab.py:227:    async def _prepare_headless(self):
# ...and that is the only hit. Defined, never invoked.

Zendriver, the community fork, calls the same method from connection.py:560 whenever headless is set. That single line is the entire difference between "passes sannysoft" and "fails three checks", and it is why the fork exists: nodriver stopped taking contributions, and the bugs stayed.

The Leak Nobody Talks About: Client Hints

So why do SeleniumBase, Botasaurus and Zendriver still score 100 while looking clean?

All three fix the UA the same way, with a CDP Network.setUserAgentOverride call carrying only a user agent string. That call has a side effect: overriding the user agent without also supplying userAgentMetadata wipes Client Hints. I measured it directly on each tool:

HeadlessChrome in UA userAgentData.brands sec-ch-ua header
Real Chrome no 3 brands present
Playwright / Patchright / Nodriver, headless yes 3 brands present
SeleniumBase / Botasaurus / Zendriver, headless no empty missing

So there are two ways to fail, and every Chromium tool picks one. Either you announce yourself as headless in the UA, or you claim to be Chrome 152 while sending no sec-ch-ua header at all - which no real Chrome does, and which a server can check without running a line of JavaScript. The three "clean" tools swapped a loud leak for a quiet one.

Fixing it means passing the metadata along with the override. Do not hardcode the brand list, because the GREASE brand (Not?A_Brand) and the version numbers change between Chrome releases. Read the real values from the browser you just launched:

import json
import zendriver as zd

async def fix_headless_identity(tab):
    """Strip 'HeadlessChrome' from the UA without losing Client Hints."""
    # Tools that already set a metadata-less override wiped the hints.
    # An empty user agent clears that override so we can read the real values.
    await tab.send(zd.cdp.network.set_user_agent_override(user_agent=""))
    await tab.reload()

    hints = await tab.evaluate(
        """navigator.userAgentData.getHighEntropyValues(
               ['architecture', 'bitness', 'model', 'platformVersion', 'fullVersionList'])
           .then(h => JSON.stringify({...h,
               platform: navigator.userAgentData.platform,
               mobile: navigator.userAgentData.mobile,
               ua: navigator.userAgent}))""",
        await_promise=True,
    )
    h = json.loads(hints)
    brand = zd.cdp.emulation.UserAgentBrandVersion
    metadata = zd.cdp.emulation.UserAgentMetadata(
        platform=h["platform"], platform_version=h["platformVersion"],
        architecture=h["architecture"], model=h["model"],
        mobile=h["mobile"], bitness=h["bitness"],
        brands=[brand(brand=b["brand"], version=b["version"].split(".")[0]) for b in h["fullVersionList"]],
        full_version_list=[brand(brand=b["brand"], version=b["version"]) for b in h["fullVersionList"]],
    )
    await tab.send(zd.cdp.network.set_user_agent_override(
        user_agent=h["ua"].replace("HeadlessChrome", "Chrome"),
        user_agent_metadata=metadata,
    ))

Note the secure-context requirement: navigator.userAgentData does not exist on about:blank, so run this after navigating to an HTTPS page.

That restores the headers. It moves CreepJS from 31% to 25% like-headless. And fingerprint-scan still says 100.

What Headless Actually Leaks: The Screen

Two things were still wrong, and neither is about automation.

CreepJS's "headless" section has exactly three checks: webDriverIsOn, hasHeadlessUA, and hasHeadlessWorkerUA. A score of 33% means one of three fired. The page UA was fixed, navigator.webdriver was clean, so it was the worker. CreepJS reads its worker values from a service worker, and a service worker is a separate CDP target that a page-level UA override never reaches. It was still reporting HeadlessChrome.

The second signal was the display. Comparing the same tool headed and headless:

Signal Headed Headless
screen 1536x864 800x600
screen.availHeight == screen.height false true
devicePixelRatio 1.25 1

Headless Chrome reports an 800x600 screen with no taskbar and no display scaling. That combination does not occur on a real desktop, and it costs nothing to check.

Reaching for --window-size makes it worse: the viewport becomes 1920x1080 while screen stays 800x600, so the page is now larger than the screen containing it, which is impossible.

Both problems have the same fix, and it is a launch flag rather than a CDP call. --user-agent applies browser-wide, so it reaches service workers and keeps Client Hints intact, which the CDP override does not. --screen-info sets the reported screen, work area and pixel ratio:

import zendriver as zd

UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " \
     "(KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36"   # your Chrome's real UA, minus "Headless"

browser = await zd.start(headless=True, browser_args=[
    f"--user-agent={UA}",
    "--screen-info={0,0 1920x1080 devicePixelRatio=1.25 workAreaBottom=48}",
    "--window-size=1536,824",
])
tab = await browser.get("https://example.com")
# zendriver applies its own metadata-less override on connect - clear it so the flag governs
await tab.send(zd.cdp.network.set_user_agent_override(user_agent=""))
Stock headless HeadlessChrome in UA page + service worker ✗ still flagged UA override (what tools do) UA fixed sec-ch-ua now missing ✗ still flagged + metadata Client Hints restored screen still 800x600 ✗ still flagged + UA & screen flags reaches the worker screen looks real ✓ clean
Each fix exposes the next leak. Fixing the user agent breaks Client Hints; restoring those leaves the 800x600 screen; only the launch flags clear all three.

Results for headless Zendriver with those two flags, four runs, identical every time:

Stock headless With both flags Real Chrome
CreepJS 31 / 33 / 0 19 / 0 / 0 19 / 0 / 0
fingerprint-scan 100 15 0
are_you_a_bot human human human
sannysoft 0 fails 0 fails n/a
sec-ch-ua missing present present
Service worker UA HeadlessChrome clean clean

Headless now matches my real browser on CreepJS exactly, and scores the same 15 on fingerprint-scan that SeleniumBase and Zendriver get when running headed. The remaining gap between 15 and 0 is something I have not identified.

The useful part is not the flags. It is that no tool in this test does this for you. Every one of them fixes navigator.webdriver and stops there, while the browser announces an 800x600 screen.

Performance

Median of three runs, full six-page sequence. Memory is peak resident set across the whole browser process tree, which is dominated by how many renderer processes Chrome spawns, so read it as a rough band rather than a precise figure.

Median launch time, headless Zendriver Zendriver: 1.4 seconds 1.4s Nodriver Nodriver: 1.4 seconds 1.4s Botasaurus Botasaurus: 1.5 seconds 1.5s SeleniumBase SeleniumBase: 3.3 seconds 3.3s Playwright Playwright: 4.0 seconds 4.0s Patchright Patchright: 4.0 seconds 4.0s Camoufox Camoufox: 10.0 seconds 10.0s Camoufox trades ~7x slower startup for the only clean headless run.
Median launch time over three headless runs, full six-page sequence.
Tool Launch, headless Launch, headed Peak RSS
Botasaurus 1.5s 1.8s ~1.4 GB
Zendriver 1.4s 1.6s ~1.4 GB
Nodriver 1.4s 1.4s ~1.4 GB
SeleniumBase 3.3s 3.4s ~1.4 GB
Playwright 4.0s 1.3s ~1.2 GB
Patchright 4.0s 1.3s ~1.6 GB
Camoufox 10.0s 10.1s ~1.0 GB

Camoufox is 7x slower to start than the CDP drivers. For a long-running session that is irrelevant. For short-lived containers that launch a browser per job, it dominates your runtime.

One honesty note on the Playwright and Patchright headless figures: individual runs were 1.1s, 5.0s and 4.0s. The median is 4.0s but the spread is real, and I would not read anything into the difference between them and the 1.4s group.

The Harness

Every tool exposes a different API, so the harness reduces each to two functions - navigate, and evaluate JavaScript - and runs identical extractors against all of them:

from contextlib import contextmanager
import tempfile, asyncio, shutil

@contextmanager
def patchright_session(headless):
    import patchright.sync_api as m
    with m.sync_playwright() as p:
        ctx = p.chromium.launch_persistent_context(
            tempfile.mkdtemp(), channel="chrome", headless=headless, no_viewport=True)
        page = ctx.pages[0]
        try:
            yield (lambda url, wait: (page.goto(url, timeout=60000), page.wait_for_timeout(wait)),
                   lambda expr: page.evaluate(expr))
        finally:
            ctx.close()

@contextmanager
def zendriver_session(headless):
    import zendriver as zd
    loop = asyncio.new_event_loop()
    browser = loop.run_until_complete(zd.start(headless=headless))
    state = {}

    async def goto(url, wait):
        state["tab"] = await browser.get(url)
        await state["tab"].sleep(wait / 1000)

    try:
        yield (lambda url, wait: loop.run_until_complete(goto(url, wait)),
               lambda expr: loop.run_until_complete(state["tab"].evaluate(expr)))
    finally:
        loop.run_until_complete(browser.stop())

SANNYSOFT = """JSON.stringify({
    passed: document.querySelectorAll('td.passed').length,
    failed: [...document.querySelectorAll('td.failed')]
              .map(td => td.parentElement.cells[0].innerText.trim())
})"""

with zendriver_session(headless=True) as (goto, js):
    goto("https://bot.sannysoft.com/", 6000)
    print(js(SANNYSOFT))

Two things that cost me time and will cost you time too. Run each tool in a separate subprocess: a crash or hang in one otherwise takes the whole matrix with it. And poll for verdicts instead of sleeping a fixed interval. deviceandbrowserinfo took over 45 seconds to render its result on some runs, and my first pass recorded those as empty. I nearly wrote them up as detections.

What Passing These Pages Does Not Mean

These are fingerprinting pages. They are not Cloudflare, DataDome or Akamai. Specifically, none of this covers:

  • IP reputation. Every run came from a residential connection. A datacenter IP changes the outcome before any JavaScript runs, and it is often the entire decision.
  • Behavior. Nothing here moves a mouse or types. Navigation that is too direct, form fills with no input events, and sessions that never idle are all signals these pages do not measure.
  • TLS consistency. Every Chromium tool produced an identical JA4 (t13d1517h2_8daaf6152771_cb7bf5808d99) because they all drive the same installed Chrome. Camoufox produced a Firefox JA4 matching its Firefox UA, which is correct. (I covered how these protocol-level fingerprints work in Fingerprinting Beyond JA3.) But Camoufox rotates its spoofed OS, and in one headless run it presented a macOS user agent while running on Windows. That run also scored its worst on fingerprint-scan, 20 instead of 5. That is a single observation, not a proven cause, but an OS-level mismatch is exactly the sort of thing a fingerprinting service cross-checks against fonts and WebGL.
  • Server environments. Every number here comes from a laptop with a real GPU and a real display. On a VPS the picture is very different, and that is the subject of the follow-up post.

Key Takeaways

  • Headed versus headless matters more than tool choice. Four different tools match a real browser exactly when headed.
  • Camoufox is the only tool that passes headless out of the box, and it costs you a ~10 second launch.
  • Every Chromium tool leaks in headless mode: either HeadlessChrome in the UA, or a metadata-less UA override that silently removes sec-ch-ua. Check the wire headers, not just navigator.userAgent.
  • Nodriver 0.50.3 ships the fix for its own UA leak and never calls it. Zendriver, the fork, calls it. That is the whole difference.
  • Headless Chrome reports an 800x600 screen with no taskbar and no display scaling. --screen-info fixes it. No tool does this for you.
  • Prefer --user-agent over a CDP override: it reaches service workers and preserves Client Hints.
  • Stop using puppeteer-extra-stealth, undetected-chromedriver and rebrowser-patches. They have not shipped in over a year.
  • Always establish a human baseline. "19% like headless" sounds bad until you see a real browser score it.

Testing was done against public fingerprinting test pages that exist for this purpose, using my own machine and connection. Nothing here targets a production site or a live protection system. Check that you have authorization before testing any system you do not own.