THE LAB #111: The OSS browser automation stealth benchmark
Fifteen tools against two live bot detectors, and only four passed the strict one.
Every few months, I see a new “undetectable” browser automation library pop up on GitHub. The README always promises the same things: no navigator.webdriver, patched fingerprints, Cloudflare bypasses. These claims are easy to make but hard to verify. If you choose the wrong tool, you might not notice at first. Your scraper keeps running, pages load, but the data slowly turns into soft-blocked junk that looks fine until you actually check it.
Give your AI a web data layer – Decodo’s Web Scraping API turns any site into clean, structured data your models can actually use.
I wanted a number I could actually trust, not just another marketing claim, and started to write this piece. Then I saw the great work on the Tyler Richards website (richards.foo/tools/stealth-benchmark) and took some inspiration from it. My idea was to take the open source Python automation stack, point each tool at the same signals a bot detector checks in JavaScript, and score them all the same way. In the past, I have written about anti-detect browsers and fingerprint test pages, but those were about commercial GUI browsers. This time, I focused on the libraries you actually pip install and use in code.
The whole test harness is in our GitHub repository reserved for paying users, inside the folder 111.STEALTH_BENCHMARK. You can run it yourself and get your own results.
The tools I put on the bench
I grouped the fifteen tools into families, so the scores are easier to compare.
My two baselines are plain Playwright and plain Selenium, with no stealth and no patches. These are the control group, and they are supposed to get caught. If a “stealth” tool scores the same as vanilla Playwright, that tells you a lot.
The Playwright stealth patches are patchright, rebrowser-playwright, and undetected-playwright. These keep the Playwright API and try to remove specific automation leaks underneath it.
The CDP-native group is pydoll, nodriver, and zendriver. They drop WebDriver entirely and talk to Chrome over the DevTools Protocol. nodriver is the official successor to undetected-chromedriver, written by the same author, and zendriver is a maintained fork of nodriver.
Undetectable browsers are getting impressively capable, but even the stealthiest one stalls when its connection lets it down. anyIP’s residential and mobile proxies pair real-user IPs with 99.9% uptime - so the network is never the weak link
Then the browsers that rebuild the fingerprint instead of patching flags on top of a normal Chrome. camoufox and invisible_playwright are patched Firefox builds that rewrite the fingerprint at the C++ level. CloakBrowser does the same to Chromium, shipping its own patched binary with a drop-in Playwright API. RayoBrowse is a self-hosted stealth Chromium from Rayobyte that runs in a Docker container and serves a fingerprint picked from a database of real devices. Obscura is the odd one out, a headless browser engine written in Rust that embeds V8 and ships its own browser rather than driving Chrome at all. And scrapling through its StealthyFetcher wraps a stealth Chromium, while undetected-chromedriver is patched Selenium.
What the detector actually sees
A bot detector is not just one thing, so before testing, you have to decide which layer you are actually targeting. In practice, a real detector looks at four main areas: the JavaScript fingerprint surface, the transport (TLS and HTTP/2), request timing, and the proxy exit. For this benchmark, I focused only on the JavaScript fingerprint surface and ignored the rest on purpose.
This is not about being lazy. Scoping it this way is what keeps the results honest. Transport fingerprinting mostly depends on the browser’s own network stack, which is the same whether you use Chromium or Firefox, no matter which Python library you use. Timing and behavior are a different topic, more about mouse movement and dwell time. Proxy reputation is about your IP, not your tool. So the real question I can answer by swapping libraries is simple: when a detector’s JavaScript runs inside the page, what does it actually find?
The detector looks for a fixed list of tells. Is navigator.webdriver true. Does the user agent say HeadlessChrome. Is the WebGL renderer a software rasterizer like SwiftShader instead of a real GPU. Are there leftover automation globals on window, like the old chromedriver cdc_ variables or a framework’s init scripts. Does the chrome object have the right properties. Do the user agent, navigator.platform, and the Client Hints all agree on the OS. The knowledge base on richards.foo lists hundreds of these, but I pulled the highest severity ones into my own probe.
How I set up the test
At the core, I use a single HTML page called probe.html, served from a small local server. Each tool loads this page, the page reads its own fingerprint in JavaScript, and writes the result back into the DOM as base64 JSON. I use base64 so I do not run into any HTML-escaping issues when I pull the rendered page from each driver.
To keep the harness manageable, I wrap every tool, no matter how different its API, in a single method:
def render(url, wait_selector=None, timeout=30000, settle_ms=6000, screenshot_path=None) -> str:
# launch in the tool's stealth config, goto(url), wait, return final HTML, closeThe new anti-bot solution - Toughest walls, lowest price. Claim your free 10 000 requests with coupon code WSCLUB
Whether it is Playwright sync, Selenium’s execute_script, pydoll and nodriver’s async CDP calls, scrapling’s StealthyFetcher.fetch, CloakBrowser’s launch(), RayoBrowse over a Docker CDP socket, or Obscura over its Rust CDP server, they all get wrapped by render(url) -> html. I use the same method for the probe and the two oracle pages, so I never have to special-case a driver. Here is the full vanilla Playwright adapter, and it does not get simpler than this:
from playwright.sync_api import sync_playwright
def render(url, wait_selector=None, timeout=30000, settle_ms=6000, screenshot_path=None):
with sync_playwright() as p:
browser = p.chromium.launch(channel="chrome", headless=True)
page = browser.new_page()
try:
page.goto(url, wait_until="load", timeout=timeout)
if wait_selector:
page.wait_for_selector(wait_selector, timeout=timeout)
else:
page.wait_for_timeout(settle_ms)
if screenshot_path:
page.screenshot(path=screenshot_path, full_page=True)
return page.content()
finally:
browser.close()For every Chrome-based tool, I used channel="chrome" to drive the real installed Chrome, not Playwright’s bundled Chromium. This choice is important. Playwright’s bundled Chromium in headless mode reports a software WebGL renderer (SwiftShader), which is just a headless artifact, not something the tool did. When I point the same tools at real Chrome, the WebGL renderer shows the actual Apple M2 GPU. By using real Chrome everywhere, the tests I measure reflect the tool’s stealth, not which Chromium binary happened to ship. The engine-level browsers like Camoufox, CloakBrowser, and Obscura ship their own binary, so they use that instead.
I ran everything in headless mode on purpose, since that is both harder and more common for scraping. Headed mode hides some of these tells automatically, and I mention that where it is relevant.
I do not just trust my own score, so I use two outside referees. My probe gives a 0 to 100 score by subtracting a weight for each tell the tool leaks. Then I check two open bot-detection pages for an independent verdict: BrowserScan and deviceandbrowserinfo. deviceandbrowserinfo is more useful because it returns a JSON breakdown, an isBot flag, and about twenty individual detections like hasWebdriverTrue, isHeadlessChrome, and isAutomatedWithCDP. I tuned the weights in my probe so the ordering matches those verdicts, and that is the calibration step. The runner also saves a screenshot of the deviceandbrowserinfo verdict for every tool, so the human or bot call is on record, not just my parser’s output.
Running it, and what came back
Fifteen tools, three targets each, and here is the table the reporter produced.
My probe score and the deviceandbrowserinfo verdict matched on 12 out of 13 tools I could get a verdict for, so that is a 92 percent match. I saved the heatmap of every tell by every tool at 111.STEALTH_BENCHMARK/results/heatmap.png, and you can see the tiers at a glance.
Four tools passed the strict referee: camoufox, CloakBrowser, RayoBrowse, and scrapling. Three of them get there the same way, by rebuilding the browser itself instead of patching automation flags onto a normal Chrome, so there is no HeadlessChrome in the user agent and nothing that reads as CDP automation. scrapling is the fourth, and it passes for a reason worth its own section below, because it took a version bump to get there. Everything that still leaves navigator.webdriver true sits at the bottom of the table, and everything in the middle is one user agent string away from a clean pass.







