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 see | Likely layer | First check |
|---|---|---|
| A selector times out or an element changed, with no challenge or block page | Script | Inspect the DOM, waits, and navigation state |
| Manual Chrome and Playwright both fail on the same network and account | Network, account, or target policy | Try a known-good IP and confirm the account is not limited |
| The workflow works locally but fails in Docker or CI | Runtime environment | Compare fonts, display mode, GPU reporting, locale, and timezone |
| Manual Chrome works, but Playwright gets a CAPTCHA or different content on the same network | Browser or automation layer | Compare 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:
- Run the same flow manually in Chrome and through Playwright on the same network.
- Compare your laptop with the Docker or CI environment where the failure happens.
- Match timezone, language, and locale to the proxy exit location.
- Reuse a persistent profile when the workflow is supposed to look like a returning session.
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:
- Headless rendering may expose a WebGL renderer that does not match a normal desktop GPU.
- A stripped Docker image often has a much smaller font set than the operating system it claims to represent.
- Plugin and browser API surfaces can differ between the automated build and regular Chrome.
- Driver bindings, execution contexts, and input behavior can differ from an ordinary interactive session.
- IP location, timezone, locale, and language may disagree with each other.
- A new browser identity with no cookies or continuity on every run looks different from a returning user.
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:
- Fewer CAPTCHAs or challenges on the same flow
- The same content a normal browser receives
- Similar stability locally and in Docker or CI
- Fewer browser-specific workarounds in the automation code
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.