NEWScrapingAnt MCP for Claude Code, Cursor & Windsurf — try it free →
Skip to main content

Playwright Stealth: 5 Libraries Tested Against Bot Detectors

· 13 min read
Satyam Tripathi
Satyam is a junior data engineer and seasoned blogger. He has created several top-ranked tutorials on different topics like web scraping, automation, and scraping tools. He is always open to working with new technologies in the market and sharing his knowledge.

Playwright Stealth: 5 Libraries Tested Against Bot Detectors

Updated 2026-09-22

Replaced the legacy Playwright 1.40 recipe and historical screenshots with five library integrations, plain-browser controls and dated BrowserScan/Sannysoft observations. A diagnostic result does not establish that a scraper is undetectable or will reach a protected target. The tested code, raw results and screenshots preserve both successful captures and excluded harness errors.

Which Playwright stealth library should you use? In this comparison, Patchright returned BrowserScan's Normal verdict when headed, while its headless user agent was flagged. Node's playwright-extra with the stealth plugin and the Firefox-based Camoufox returned Normal in both modes. Python playwright-stealth had no failed Sannysoft checks but still received BrowserScan's Navigator flag.

That difference is the useful result: changing a browser property, passing a diagnostic page and successfully retrieving your target are separate tests. Start with a reproducible control and keep the browser version, operating system and launch mode attached to every observation.

Start with a local Playwright control​

The evidence packet includes a small HTML fixture that writes its own browser properties into the page. The scripts read that DOM output. This avoids confusing properties read from an automation evaluation world with what the page's own script observes.

Clone the examples repository, select the dated packet tag and enter its directory:

git clone https://github.com/ScrapingAnt/scrapingant-examples.git
cd scrapingant-examples
git checkout playwright-scraping-undetectable/2026-09-22
cd examples/playwright-scraping-undetectable

Use Python 3.12 and an installed Google Chrome. The full walkthrough below reproduces the tested macOS environment in a POSIX shell. The separate monthly Linux job covers only the three Python Chromium fixture adapters; the full Node/Firefox walkthrough was not tested on Linux or Windows. The primary requirements pin Playwright and Patchright 1.63.0 and Python playwright-stealth 2.0.3.

python3.12 -m venv .venv
. .venv/bin/activate
python -m pip install -r requirements.txt
BROWSER_CHANNEL=chrome ./run.sh

The default runner executes the plain Playwright, Patchright and Python stealth files. To run only the control:

BROWSER_CHANNEL=chrome python 01_playwright.py

Captured local output:

{"approach": "plain-chrome", "browser": "154.0.8037.57", "platform": "MacIntel", "webdriver": true}

Here webdriver is the fixture's navigator.webdriver value. It is not an overall detector verdict. The helper also records the actual browser version and platform, validates fixture output and closes the browser on failure.

Version boundary: this later local run used Chrome 154.0.8037.57. The public detector table below was captured earlier on Chrome 153.0.8010.53. The local output does not revalidate that table on Chrome 154. All observations were made on September 22, 2026, on macOS 26.6.2 arm64 with Python 3.12.11; Node examples used Node 22.20.0.

For a walkthrough of browser navigation and extraction, start with Playwright web scraping in Python.

Five approaches and their integration costs​

ApproachTested packageIntegration and constraint
Patchright1.63.0Import Patchright's Playwright implementation; this comparison uses Chromium.
Python playwright-stealth2.0.3Wrap standard Playwright with Stealth().use_sync(...); inspect the resulting fingerprint values.
playwright-extra + stealth plugin4.3.6 + 2.11.2Node plugin integration around Playwright 1.63.0. These selected package releases date to March 2023.
Rebrowser Playwright1.52.0Node Playwright fork; the selected May 2025 release uses an older driver generation than the other Chromium adapters.
CamoufoxPython 0.5.6Separate Firefox-based browser, build 152.0.4-beta.30, with Playwright 1.62.0 in its own environment.

The old article used undetected-playwright-python; its repository was marked archived when inspected for this update. The replacements above were actually launched and measured. An older release date alone does not prove a package is abandoned, just as a newer version does not establish better detection results.

1. Patchright: start with the headed/headless distinction​

From the cloned packet, 02_patchright.py is:

from patchright.sync_api import sync_playwright
from common import capture, options

with sync_playwright() as p:
browser = p.chromium.launch(**options())
try:
capture(browser, "patchright")
finally:
browser.close()

common.py is the packet's shared fixture helper. The local example runs headless; it is a property demonstration, not the headed BrowserScan test. Run it in the activated primary environment:

BROWSER_CHANNEL=chrome python 02_patchright.py
{"approach": "patchright", "browser": "154.0.8037.57", "platform": "MacIntel", "webdriver": false}

Patchright changed the observed webdriver value to false in this fixture. In the separate Chrome 153 public comparison, that did not hide the headless User-Agent category. The headed configuration returned Normal in both BrowserScan observations.

Use it when: you want a Python/Chromium option to evaluate, especially if headed operation is acceptable. A tuned headless recipe or persistent-context configuration would need its own evidence; neither was added after seeing these results.

2. Python playwright-stealth: inspect consistency, not just one flag​

The tested wrapper in 03_python_stealth.py is:

from playwright.sync_api import sync_playwright
from playwright_stealth import Stealth
from common import capture, options

with Stealth().use_sync(sync_playwright()) as p:
browser = p.chromium.launch(**options())
try:
capture(browser, "python-stealth")
finally:
browser.close()

Run it with the same primary environment and installed Chrome:

BROWSER_CHANNEL=chrome python 03_python_stealth.py
{"approach": "python-stealth", "browser": "154.0.8037.57", "platform": "Win32", "webdriver": false}

The local fixture reported Win32 on this Mac, while the ordinary Chrome control reported MacIntel. In the public comparison, Python stealth's Macintosh user agent and changed platform appeared alongside a BrowserScan Navigator flag. These observations suggest a consistency issue worth investigating; they do not isolate the detector's decision rule or prove that platform alone caused the flag.

Use it when: you want to evaluate this wrapper in an existing Python Playwright application and can check the resulting values on your actual host. Sannysoft alone would have missed the problem shown here: it marked none of the captured checks as failed in either mode.

3. playwright-extra with the stealth plugin: the Node alternative​

With Node 22 and npm available, install the exact locked Node dependencies from the packet directory:

npm ci --ignore-scripts
BROWSER_CHANNEL=chrome node 04_extra_stealth.cjs

The adapter registers the plugin before launching:

const { chromium } = require('playwright-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
const { capture } = require('./common.cjs');

chromium.use(StealthPlugin());
capture(chromium, 'extra-stealth').catch(error => {
console.error(error); process.exitCode = 1;
});

Captured local output:

{"approach":"extra-stealth","browser":"154.0.8037.57","webdriver":false,"platform":"MacIntel"}

The separate public comparison returned BrowserScan Normal in both modes and no failed Sannysoft checks. This was the strongest headless Chromium result among these tested defaults, on these two pages. It is not a measured target-site success rate.

Use it when: Node is suitable and you can validate the locked integration after browser updates. Keep the March 2023 package release dates visible in that compatibility decision; the observed result is useful without assuming indefinite compatibility.

4. Rebrowser: measure the default profile as well as the driver​

The same Node install includes the pinned Rebrowser package:

BROWSER_CHANNEL=chrome node 05_rebrowser.cjs

Its adapter uses chromium from rebrowser-playwright and the same fixture helper:

const { chromium } = require('rebrowser-playwright');
const { capture } = require('./common.cjs');

capture(chromium, 'rebrowser').catch(error => {
console.error(error); process.exitCode = 1;
});
{"approach":"rebrowser","browser":"154.0.8037.57","webdriver":true,"platform":"MacIntel"}

Rebrowser retained webdriver: true in this local run. In the public comparison it received the same named BrowserScan flags as the ordinary Chrome control: Webdriver and User-Agent headless, Webdriver headed. No extra stealth plugin or custom launch flags were layered onto it.

Use it when: you have a reason to evaluate this particular fork and can test the driver/browser combination you will deploy. These measurements do not establish an advantage over plain Playwright on the selected suites. In particular, BrowserScan left CDP unflagged even for the plain control, so there was no positive CDP control against which to demonstrate a patch benefit.

5. Camoufox: evaluate Firefox separately​

Camoufox 0.5.6 requires Playwright below 1.63. Keep it out of the primary environment. The supplied full dependency freeze is macOS-specific (it includes PyObjC); use these installation commands on macOS, not as a portable Linux/Windows recipe:

python3.12 -m venv .venv-camoufox
.venv-camoufox/bin/python -m pip install -r requirements-camoufox.txt
.venv-camoufox/bin/python -m camoufox fetch official/152.0.4-beta.30
.venv-camoufox/bin/python -m playwright install firefox
.venv-camoufox/bin/python 06_firefox.py
.venv-camoufox/bin/python 07_camoufox.py

The first script is the ordinary Firefox control; the second is:

import platform
from camoufox.sync_api import Camoufox
from common import capture

host_os = {"Darwin": "macos", "Linux": "linux", "Windows": "windows"}[platform.system()]
with Camoufox(headless=True, os=host_os, browser="152.0.4-beta.30") as browser:
capture(browser, "camoufox")

Captured Camoufox output:

{"approach": "camoufox", "browser": "152.0.4-beta.30", "platform": "MacIntel", "webdriver": false}

The ordinary Firefox fixture reported webdriver: true; Camoufox reported false. In BrowserScan, Firefox received Robot/Webdriver in both modes and Camoufox received Normal. The control used Firefox 153.0 while Camoufox used its own 152.0.4-beta.30 build. This changes the browser build and fingerprint defaults together, so it does not isolate a single patch's effect.

Use it when: a Firefox-based workflow is acceptable and you can evaluate its compatibility with your target. Keep the pinned beta build and separate dependency environment in the deployment decision. The tested script constrains fingerprint OS to the host; other fingerprint defaults can vary between launches.

BrowserScan results: defaults, modes and controls​

We captured two fresh-browser observations for each of seven configurations in each mode: 28 BrowserScan observations. Both repetitions agreed in every table cell. The named flags below are BrowserScan's red summary categories.

ConfigurationHeadlessHeaded
Plain Playwright ChromeRobot: Webdriver, User-AgentRobot: Webdriver
PatchrightRobot: User-AgentNormal
Python playwright-stealthRobot: NavigatorRobot: Navigator
playwright-extra + stealth pluginNormalNormal
Rebrowser PlaywrightRobot: Webdriver, User-AgentRobot: Webdriver
Plain Playwright FirefoxRobot: WebdriverRobot: Webdriver
CamoufoxNormalNormal

The target was BrowserScan's bot-detection page, not a commercial scraping target. All Chromium rows used the same installed Chrome 153 binary; the framework versions still differ. Firefox is a separate comparison.

BrowserScan shows Robot with Webdriver and User-Agent flagged for the plain headless Chrome control

BrowserScan shows Normal for headed Patchright in the measured Chrome 153 configuration

CDP was unflagged even in plain Chrome. Do not read this table as proof that any library defeated a functioning CDP-positive test. That is why the old article's broad CDP inference has been removed.

Cross-check with Sannysoft​

We also captured Sannysoft once per configuration and mode: 14 observations, each with 31 recognized classified checks. No warning states were recorded. “None” below means no captured check had the page's failed state.

ConfigurationHeadless: failed checksHeaded: failed checks
Plain Chrome and RebrowserUser Agent (Old), WebDriver (New), HEADCHR_UA, CHR_MEMORYWebDriver (New)
PatchrightUser Agent (Old), HEADCHR_UA, CHR_MEMORYNone
Python stealth and Node stealthNoneNone
Plain FirefoxWebDriver (New), Chrome (New), Plugins Length (Old), Plugins is of type PluginArraySame four checks
CamoufoxChrome (New)Chrome (New)

Chrome (New) is Chrome-specific. Its failure in Firefox is not a fair basis for a cross-engine automation score. Likewise, a check name alone does not explain whether it affects access to your target.

The practical warning is Python stealth: Sannysoft displayed no failures, while BrowserScan flagged Navigator. Keep both observations instead of declaring the library undetectable from whichever page gives the preferred answer.

Reproduce, handle failures and choose a next step​

With both Python environments and Node dependencies installed, run every local adapter:

BROWSER_CHANNEL=chrome CAMOUFOX_PYTHON=.venv-camoufox/bin/python ./run.sh --all

The packet's default monthly CI job runs only the three Python Chromium fixture adapters, using bundled Chromium. It does not call the public detectors or assert that a stealth library remains undetected. The recorded full local run covers all seven adapters.

For a missing browser executable, install the browser selected by your configuration: Google Chrome for BROWSER_CHANNEL=chrome, or Playwright's matched Chromium for default mode. Keep Camoufox's requirements in its own environment. A missing fixture, malformed result, navigation timeout or unfamiliar detector schema is an execution/collection error, never a pass.

For a new public comparison, follow the diagnostic harness instructions. Use a new dated output directory and record the installed browser version. Do not overwrite an old result or transfer it to a new browser version without running the test.

The starting shortlist from these measurements is Patchright for headed Chromium, Camoufox for headless Firefox, and playwright-extra with stealth for headless Chromium. Choose the engine and runtime your application can support, then test the actual access task separately. Proxy configuration is another part of that task, not something this fingerprint comparison measured.

For that separate setup, see rotating proxies in Playwright.

ScrapingAnt is not needed to run any example or diagnostic here. If you are evaluating an API workflow separately, start with the ScrapingAnt documentation. No ScrapingAnt API or paid proxy was used in these measurements, and this comparison makes no claim about their detection or CAPTCHA outcomes.

Limits of the evidence​

Runs used fresh browsers and contexts on one machine/network, without proxy or account state. Public-page result snapshots were captured six and ten seconds after DOMContentLoaded, followed by screenshots; the recorded check states agreed at those two instants. Reading the page through automation is itself an interaction, so the observations are not observer-free or proof of permanent stability.

Two initial Node harness runs failed to serialize their result snapshots. They were archived and replaced after the main matrix, not counted as detections or passes. Three separate screenshot-layout visits are also retained: the JavaScript stealth configurations produced a 4000-pixel-wide BrowserScan page outside the 1440-pixel viewport; full-page images agreed with the saved DOM. Those visits are not additional matrix repetitions.

The local fixture uses a file URL. The public sample is small, Sannysoft was not repeated, and package defaults change several properties at once. No target-site success probability, CAPTCHA solving, IP reputation, TLS fingerprint, performance or long-running reliability was measured. A Normal result means only that this version of that diagnostic page reported Normal for the captured configuration.

Examples tested on 2026-09-22 with Python 3.12.11, Playwright/Patchright 1.63.0, playwright-stealth 2.0.3, Node 22.20.0, playwright-extra 4.3.6, stealth plugin 2.11.2, Rebrowser Playwright 1.52.0 and Camoufox 0.5.6 with Playwright 1.62.0. Code: pinned evidence packet and raw observations. Browser versions are separated by measurement phase above.

This article was drafted with AI assistance from a tested evidence packet. Oleg Kulyk is responsible for review of this update, its code, measurements and corrections.

Forget about getting blocked while scraping the Web

Try out ScrapingAnt Web Scraping API with thousands of proxy servers and an entire headless Chrome cluster