← Blog

Playwright detected as bot? Here's what's actually happening

You open the target in Chrome and everything works. Then you run the same flow through Playwright. Instead of the page you expected, you get a CAPTCHA, a 403, a login loop - or a page that looks normal but quietly returns different data.

That last failure is the dangerous one. The script keeps running, the selectors still pass, and nobody notices until the output stops making sense.

Sometimes this happens on the first run. Sometimes a workflow that worked for weeks suddenly starts failing. Either way, the next useful question is the same: is the problem in the script, the network or account, the runtime environment, or the browser session itself?

Quick answer: stock Playwright can expose automation-related browser state before your script reaches the target. But a flagged IP, an empty profile, a Docker fingerprint, or a broken selector can produce similar symptoms. Compare the same flow under controlled conditions before changing tools.

Find the layer that is actually failing

Start with the failure you can observe, not the fix you expect to need:

What you seeLikely layerFirst check
A selector times out or an element changed, with no challenge or block pageScriptInspect the DOM, waits, and navigation state
Manual Chrome and Playwright both fail on the same network and accountNetwork, account, or target policyTry a known-good IP and confirm the account is not limited
The workflow works locally but fails in Docker or CIRuntime environmentCompare fonts, display mode, GPU reporting, locale, and timezone
Manual Chrome works, but Playwright gets a CAPTCHA or different content on the same networkBrowser or automation layerCompare browser state and launch configuration

These are signals, not proofs. A site can combine IP reputation, account history, browser fingerprinting, and behavior in the same decision. The point is to stop treating every 403 as a Playwright problem - or every Playwright failure as a bad proxy.

A few checks narrow it down quickly:

If stock Chrome works consistently while the automated session is challenged under the same conditions, the browser and automation layer becomes the useful place to investigate.

Why Playwright is detected as a bot

Start with the standard navigator.webdriver property:

import asyncio
from playwright.async_api import async_playwright

async def main():
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        page = await browser.new_page()
        print(await page.evaluate("navigator.webdriver"))
        await browser.close()

asyncio.run(main())

With a default Playwright Chromium launch, this prints True before you navigate to a site.

That does not mean CDP by itself always turns the property on. Stock Playwright launches Chromium with automation-related defaults, and those launch conditions are what make the browser report that it is controlled. A browser started separately and later connected over CDP can behave differently. The result identifies an automation-enabled browser state; it does not uniquely identify Playwright, Puppeteer, or Selenium.

The property is cheap for a page to read, so it remains a useful first-pass signal. A public checker such as Sannysoft’s bot test makes the result easy to see, but serious detection systems rarely stop there.

The rest of the browser fingerprint

Automation detection can combine several kinds of evidence:

A simple detector may act on one obvious signal. Mature systems usually combine browser, network, session, and behavioral evidence into a score. That is why changing one property can improve a public test page without fixing the production workflow that sent you searching in the first place.

Why a stealth plugin is only a partial fix

The usual first step is playwright-extra with a stealth add-on, or the Puppeteer equivalent. Modern stealth plugins do more than add one naive getter: they can change launch flags, patch selected browser APIs, preserve property descriptors, and make modified functions look native.

That can help. It is still a compatibility layer applied around stock Chromium, built from a finite list of evasions. Browser releases change the underlying APIs, detectors compare values across multiple surfaces, and a patch that fixes one check can create a new inconsistency somewhere else. The plugin and the detector then have to keep adapting to each other.

The stealth add-on also does not, by itself, make interaction behavior human. Playwright’s default pointer movement reaches its target in one step, and form actions are designed for deterministic automation rather than human variation. Behavior is a separate layer from the browser fingerprint.

Move the change into the browser

Source-level browser changes remove a class of artifacts created by JavaScript overrides. The browser returns its values through its normal native interfaces instead of placing a runtime patch over the page-visible property.

That is the approach behind CloakBrowser: a custom Chromium build with source-level fingerprint changes, plus wrappers that preserve the Playwright and Puppeteer APIs. For an initial comparison, the Python change is small:

from cloakbrowser import launch

browser = launch()
page = browser.new_page()
page.goto("https://example.com")
browser.close()

For a protected production target, test closer to the environment you will actually use:

from cloakbrowser import launch

browser = launch(
    proxy="http://user:pass@residential-proxy:port",
    geoip=True,
    headless=False,
    humanize=True,
)

page = browser.new_page()
page.goto("https://example.com")
browser.close()

geoip=True aligns timezone and locale with the proxy exit IP. It requires the GeoIP extra in Python: pip install "cloakbrowser[geoip]". humanize=True applies human-like mouse, keyboard, and scroll behavior through the wrapper. Headed mode is worth including when the target treats headless sessions differently.

Source-level changes are not a guarantee of permanent undetectability. They reduce browser and driver signals; they do not repair a burned proxy, an account restriction, an impossible request rate, or a workflow that behaves unlike a user. Browser and detection vendors also keep shipping updates, so any result has to be re-tested against the real target.

To compare it with your current setup, install CloakBrowser, keep the network and workflow constant, and change the browser layer. The CloakBrowser quickstart shows the Python, Playwright, and Puppeteer paths.

Test it like a production dependency

Do not decide from one lucky run on a public checker. Test the same target flow several times with the same account, proxy, profile, and environment. Then change one variable at a time.

Useful outcomes include:

The goal is not a perfect score on every fingerprint page. It is a repeatable improvement on the workflow that needs to run.

Frequently asked questions

Does CloakBrowser guarantee that a site will not block me?

No. It reduces browser- and driver-level automation signals. A target can still block an IP, account, traffic pattern, or action sequence for reasons unrelated to the browser fingerprint.

Is CloakBrowser a replacement for Playwright?

It is a drop-in replacement at the browser launch and integration layer, not a new automation API. Your selectors, page methods, and test structure still use the familiar Playwright model.

Do I need to rewrite my scripts?

The first comparison is usually an import swap. A production setup may also need a suitable proxy, GeoIP alignment, a persistent profile, or humanized interactions, depending on the target.

Should I trust a public bot-detection test?

Use it as a diagnostic, not a verdict. Public tests expose obvious browser differences, while a production target can also score network reputation, account history, session continuity, and behavior.

Run it against the workflow that is actually blocked

The only result that settles the question is a controlled comparison on your target, with your script and your network conditions.

pip install cloakbrowser or npm install cloakbrowser, swap the import, and run the same flow again.