Why Your Headless Browser Gets Detected (and How to Fix It)
Headless Chrome and Firefox leak dozens of signals that anti-bot systems catch instantly. Here is exactly what gives you away and the specific fixes for each.
Yash Dubey
February 7, 2026
You launch headless Chrome, navigate to a page, and get a 403 or a CAPTCHA. The site knows you are a bot. But how?
Headless browsers leak identity signals at every layer. TLS handshake, HTTP headers, JavaScript APIs, rendering behavior - each one is a potential detection vector. Modern anti-bot systems like Cloudflare, DataDome, and PerimeterX check all of them.
The Detection Signals
navigator.webdriver
The most obvious one. When Chrome runs in automation mode, navigator.webdriver returns true. Every anti-bot system checks this first.
// What bots show
navigator.webdriver // true
// What real browsers show
navigator.webdriver // undefined or falseFix: Override it before the page loads.
# Playwright
page.add_init_script("Object.defineProperty(navigator, 'webdriver', {get: () => undefined})")Chrome DevTools Protocol Markers
ChromeDriver injects cdc_ variables into the page context. These are internal markers for the automation framework, and anti-bot systems scan for them.
// Detection check sites run
for (let key in document) {
if (key.match(/cdc_|\$chrome_|\$cdc_/)) {
// Bot detected
}
}Fix: Use Playwright instead of Selenium. Playwright connects via a different protocol and does not inject these markers.
Missing Browser Plugins
A real Chrome installation has plugins - PDF viewer, Chrome PDF Viewer, Native Client. Headless Chrome reports zero plugins by default.
// Real browser
navigator.plugins.length // 3-5
// Headless
navigator.plugins.length // 0Fix: Inject fake plugin arrays via init scripts, or run in headed mode.
Canvas and WebGL Fingerprints
Headless browsers produce different canvas rendering output than headed ones. Anti-bot systems render a hidden canvas element and hash the result. Headless Chrome on Linux produces a distinctly different hash than Chrome on Windows or macOS.
This is hard to fake convincingly. The rendering differences come from the GPU drivers and font rendering stack, not from JavaScript.
TLS Fingerprint (JA3/JA4)
This happens before any JavaScript runs. Your HTTP client and browser version produce a unique TLS handshake signature. Headless Chrome has a different JA3 hash than regular Chrome because the cipher suite ordering differs slightly.
Python requests, Node axios, Go net/http - they all have completely different TLS fingerprints from any browser. This is often the first thing that gets you blocked.
HTTP/2 Settings
Real browsers use HTTP/2 with specific settings frames (SETTINGS_MAX_CONCURRENT_STREAMS, SETTINGS_INITIAL_WINDOW_SIZE, etc.). The values differ between Chrome, Firefox, and Safari. Most HTTP clients either do not support HTTP/2 or use default values that do not match any browser.
The Realistic Fix Strategy
For Small Scale (under 1K pages/day)
Run a patched Playwright instance with stealth plugins:
from playwright.sync_api import sync_playwright
def create_stealth_browser():
p = sync_playwright().start()
browser = p.chromium.launch(
headless=False, # headed mode avoids many detections
args=[
"--disable-blink-features=AutomationControlled",
"--no-first-run",
"--no-default-browser-check",
]
)
context = browser.new_context(
viewport={"width": 1920, "height": 1080},
locale="en-US",
)
return browser, contextAdd random delays between actions. Real users do not click links at machine speed.
For Medium Scale (1K-50K pages/day)
Patched browsers get expensive at this scale. Each browser instance needs 200-500 MB of RAM. At 50K pages per day, you are running 10-20 concurrent instances.
This is where most teams either build custom infrastructure or switch to a scraping API. The infrastructure approach means managing proxy rotation, browser pools, session handling, and monitoring. That is a product in itself.
For Large Scale (50K+ pages/day)
At this volume, the only approaches that make economic sense are dedicated scraping infrastructure or API services that have already solved these problems. The per-page cost needs to be under $0.001 to make the unit economics work.
AlterLab handles the browser fingerprinting, proxy rotation, and anti-bot bypass at this tier automatically. You send a URL, you get back data. The hard targets with JS rendering and anti-bot bypass cost more per request, but you are not paying for the easy ones at the same rate.
Quick Detection Test
Want to see how detectable your setup is? Check these:
// Run these in your headless browser console
console.log("webdriver:", navigator.webdriver);
console.log("plugins:", navigator.plugins.length);
console.log("languages:", navigator.languages);
console.log("platform:", navigator.platform);
console.log("hardwareConcurrency:", navigator.hardwareConcurrency);If webdriver is true, plugins is 0, or hardwareConcurrency is undefined, you are getting caught at the most basic level.
The goal is not perfect stealth. The goal is passing enough checks that the anti-bot system classifies you as a low-risk visitor rather than an obvious bot.