You moved from raw HTTP to a real browser. TLS fingerprint: genuinely Chrome's. IP: residential. And the site still knows. In 2026, headless browser detection has moved far past navigator.webdriver — the current generation of checks detects the automation protocol itself, and most "stealth" plugins patch yesterday's signals while loudly exhibiting today's.
This article maps what sites actually check now, roughly in the order they check it, and what genuinely helps at each layer. It completes the detection trilogy with JA3/JA4 TLS fingerprinting (network layer) and IP reputation & ASN (address layer) — this is the browser layer.
navigator.webdriver — spec-required to be true under automation. Playwright/Puppeteer stealth setups hide it; Chrome's --disable-blink-features=AutomationControlled handles it natively. Sites still check it because it's free and catches naive bots.HeadlessChrome/ in the User-Agent. Fixed by the new headless mode (headless=new), which uses the real Chrome binary path.navigator.plugins, no chrome.runtime, 0×0 outerWidth. Old headless mode failed all of these; new headless mostly passes.If a target only checks Tier 1, modern Playwright with headless=new walks through. Serious targets moved on years ago.
Playwright and Puppeteer drive Chrome over the Chrome DevTools Protocol, and CDP leaves runtime side effects that page JavaScript can observe:
// The classic CDP leak: serialization callbacks
const err = new Error();
Object.defineProperty(err, "stack", {
get() {
// This getter fires DURING console serialization -
// which only happens when DevTools/CDP is attached
window.__cdp_detected = true;
},
});
console.debug(err);
Variants of this — getter side effects during console serialization, timing anomalies in Runtime.evaluate, the behavior of toString() on patched native functions — are in every commercial anti-bot bundle. The crucial point: this detects the protocol, not the headless mode. A headed, stealth-patched, perfectly human-looking Chrome still fails if it's driven over CDP.
What helps:
Beyond artifacts, modern systems check whether your browser's story is internally consistent:
| Check | Bot tell |
|---|---|
| Canvas / WebGL render hash | SwiftShader/llvmpipe software rendering = server GPU; or a hash shared by 10,000 "different users" |
| Fonts & codecs | Linux server font set under a Windows User-Agent |
| Timezone × locale × IP geo | Intl says UTC, IP says Texas, Accept-Language says de-DE |
| Screen metrics | 1920×1080 with zero taskbar, devicePixelRatio exactly 1, window never resized |
| Hardware concurrency / memory | 96 cores reported to a page claiming to be a phone |
| Behavioral micro-signals | No mouse entropy, instant form fills, scrolls in identical 100px steps |
This is where DIY stealth dies: every patch you apply must agree with every other signal. A spoofed Windows UA on a Linux container contradicts fonts, GPU strings, and TCP fingerprints simultaneously. Coherence beats perfection — an honest headed Linux Chrome scores better than a badly-spoofed "Windows" one.
None of this matters if the address layer is burned: a flawless browser on a datacenter ASN is still "a flawless browser in a datacenter" — and headless detection thresholds are adaptive to IP reputation. The same browser gets more JavaScript challenges from a flagged IP. Residential exits effectively lower the scrutiny the browser layer has to survive:
browser = p.chromium.launch(
headless=False, # headed survives more checks
args=["--disable-blink-features=AutomationControlled"],
proxy={
"server": "us.jibaoproxy.com:913",
"username": "USERNAME", "password": "PASSWORD",
},
)
toString() is itself a signal. Fewer, coherent patches win.Residential IPs that match the story your fingerprint tells — $5 free credit, no card required.
Start Free TrialNew users get $5 USDT instantly, plus an extra first-deposit reward — limited-time offer.