Learn how sites detect Selenium, Puppeteer, and Playwright via navigator.webdriver, CDP artifacts, missing features, and rendering tells.
Headless browsers power a huge share of the web's automation: CI test suites, uptime monitors, price scrapers, and—on the other side—credential-stuffers, scalpers, and click-fraud bots. Detection has evolved well beyond the era when the user-agent string literally contained HeadlessChrome. Today, sites catch headless automation through a layered web of signals that a browser controlled by code consistently leaks. This guide explains exactly which signals those are and why they are hard to suppress.
Key Takeaways
navigator.webdriveris the canonical automation flag, set totrueby any WebDriver-driven browser; patching it wrong is itself a fingerprint.- CDP artifacts are hard to fully erase: the Chrome DevTools Protocol leaves distinctive globals and timing patterns that survive many stealth patches.
- Feature gaps expose headless runtimes: software-only WebGL renderers, missing audio codecs, contradictory permissions states, and an empty plugin list all look different from a real desktop browser.
- Selenium, Puppeteer, and Playwright leave distinct signatures even after the user-agent is spoofed—each framework has known artifacts in the global scope and different default network behaviors.
- Stealth plugins raise the bar but cannot close it: they patch JavaScript-layer leaks but cannot touch signals below that layer—TLS fingerprints, HTTP/2 frame ordering, or OS-level process trees.
What Is a Headless Browser?
A headless browser runs a full rendering engine—HTML parsing, CSS layout, JavaScript execution—without drawing anything to a visible screen. A program sends instructions over a control protocol, the browser responds, and no human needs to be watching. Puppeteer, Playwright, and Selenium WebDriver all use this model, typically against Chrome Headless (launched with the --headless flag) or Firefox in headless mode.
The property that makes headless browsers useful for testing is exactly what makes them detectable: they are controlled by code, and code leaves traces.
The navigator.webdriver Tell
The most direct headless signal is navigator.webdriver. The WebDriver specification requires any browser under WebDriver control to expose this property as true. In a real desktop browser, it returns false or is undefined.
// A detector reads this in a single line.
if (navigator.webdriver === true) {
// Browser is almost certainly under WebDriver control.
}
This alone is a weak signal—stealth patches have suppressed it for years. But patching it incorrectly is a tell of its own. If the property descriptor looks non-standard, the get function is serializable, or Object.getOwnPropertyDescriptor(navigator, 'webdriver') returns an object that differs from what a real browser exposes, a careful detector notices. A badly patched navigator.webdriver = false is often more suspicious than an unpatched true.
CDP and DevTools Protocol Artifacts
Puppeteer and Playwright both drive Chrome through the Chrome DevTools Protocol—the same protocol behind Chrome DevTools. That control channel leaves artifacts in the page.
Global Leaks
Some CDP usage patterns inject objects into the page's global scope. Historical Puppeteer versions exposed __puppeteer_evaluation_script__; other frameworks leave __nightmare, _phantom, or window.callPhantom. Detection scripts enumerate these:
const knownArtifacts = [
'__nightmare', '_phantom', 'callPhantom',
'__puppeteer_evaluation_script__',
'__selenium_evaluate', '__webdriver_evaluate',
];
const found = knownArtifacts.filter(k => k in window);
// Any hit is a strong automation signal.
Event Listener Anomalies
Automation frameworks that intercept fetch or XMLHttpRequest to modify responses leave patterns in the event listener map. A detector that queries getEventListeners on the window object can see listener stacks that a real user session does not produce.
Timing Side-Channels
CDP communication has measurable latency. A sequence of tasks timed against performance.now() can reveal the characteristic jitter that appears when each step requires a round-trip through the CDP connection, rather than running synchronously on the browser's event loop.
Selenium vs Puppeteer vs Playwright Signatures
All three frameworks ultimately drive a browser, but each leaves different tracks.
| Framework | Control interface | Notable artifacts |
|---|---|---|
| Selenium | W3C WebDriver over HTTP | navigator.webdriver = true (unpatched), WebDriver-specific HTTP header order from the client library |
| Puppeteer | Chrome DevTools Protocol | CDP global injections, older window.__puppeteer_* variables, Chrome-only so TLS matches real Chrome |
| Playwright | CDP (Chrome/Edge), internal protocols (Firefox/WebKit) | Cleaner defaults than Puppeteer; Firefox/WebKit variants diverge from their real-browser counterparts in feature sets |
Playwright has invested more in reducing low-level leaks, which is why detection research now focuses on the subtler signals above when Playwright is the suspected framework.
Missing and Inconsistent Browser Features
Headless environments consistently diverge from real desktop browsers across several feature areas. Detection scripts probe these gaps in combination.
Plugin List
Real desktop Chrome ships a set of plugins (PDF Viewer, etc.) accessible via navigator.plugins. Headless Chrome historically returned an empty array. Modern builds have improved this, but the plugin objects themselves still differ in their property descriptors and can be cross-checked against what a real user's browser would expose.
Media Codecs
Real browsers negotiate codecs with the OS and underlying GPU. Headless environments running without display hardware often report limited codec support via HTMLMediaElement.canPlayType(). A browser claiming to be Chrome on Windows but reporting that it cannot play H.264 video is a meaningful contradiction.
WebGL and GPU Rendering
WebGL is one of the strongest headless tells. Without physical GPU access, Chrome Headless falls back to SwiftShader, its software rasterizer. A detector reads gl.getParameter(gl.RENDERER) and checks for strings like SwiftShader or Google SwiftShader rather than a real GPU name:
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl');
const renderer = gl.getParameter(gl.RENDERER);
// "ANGLE (Intel, ...)" or "Apple M2" → real hardware
// "Google SwiftShader" → headless software render
A user-agent claiming a powerful device while the WebGL renderer reports CPU-only software rendering is a direct contradiction.
Permissions API Contradictions
The Permissions API lets scripts query whether a user has granted access to specific capabilities. In headless environments, the internal permission store can sit in states that are impossible in a real browser session—for example, notifications simultaneously denied and prompt. Detection scripts that probe multiple permissions and check for logical consistency catch these invalid combinations.
How Stealth Plugins Try (and Often Fail) to Hide
Libraries like puppeteer-extra-plugin-stealth patch many of the signals above before any detection script can read them: they override navigator.webdriver, inject realistic plugin lists, stub CDP-specific globals, and normalize the permissions API response. Against naive detection, this works well.
The problem is structural. First, every patch is a potential tell: Object.defineProperty wrappers have different toString() output than native properties, and probing Object.getOwnPropertyDescriptor(navigator, 'webdriver') reveals an artificial descriptor. Detection systems now look for the presence of a patch as a signal. Second, stealth plugins operate entirely in the JavaScript layer. They cannot touch:
- TLS ClientHello fingerprint — the cipher suites and extensions your browser offers in the handshake reflect the underlying network library (BoringSSL in Chromium), not the JavaScript layer. A headless Chrome and a real Chrome share the same TLS fingerprint, but a Python script pretending to be Chrome does not.
- HTTP/2 frame ordering — the sequence and priority of HTTP/2 frames differ between real browsers and automation clients built on top of them.
- OS process tree — a server-side check that sees a ChromeDriver or geckodriver process alongside the browser process has out-of-band evidence of automation that no JavaScript patch can remove.
Testing Your Own Automation Setup
If you run headless automation for legitimate purposes—CI pipelines, accessibility audits, uptime checks—you can see exactly what detection systems observe about your sessions. BrowserInsight's bot detection tool reports your navigator.webdriver state, plugin count, WebGL renderer, and other automation signals in real time. The kernel check reveals which rendering engine is actually running beneath the user-agent claim. Use these to audit your setup before it goes to production.
For the network-layer signals that JavaScript cannot see, the complementary article on bot detection techniques covers TLS and HTTP/2 fingerprinting.
Frequently Asked Questions
Does patching navigator.webdriver fool detection systems?
Only against simple checks. The property's absence is expected in a real browser, but the way it is absent matters. A native false and a patched false look different when a detector reads the property descriptor. Combine patching with WebGL and plugin checks and the remaining signal is enough to classify most automation.
Can Playwright evade detection better than Selenium?
Playwright's defaults are cleaner than classic Selenium, and its cross-browser support means Firefox and WebKit drivers avoid some Chrome-specific CDP artifacts. But detection systems track Playwright-specific signals the same way they track Selenium's. No framework is currently invisible to a detection stack that tests the full signal set.
Why does headless Chrome report SwiftShader?
SwiftShader is Chrome's CPU-based software renderer—used when no real GPU is available. A server or container running headless Chrome typically has no physical display adapter, so the GPU falls back to SwiftShader. Real desktop Chrome reports the actual GPU driver name: Intel, NVIDIA, AMD, or Apple Silicon.
Are there headless setups that pass every check?
Theoretically yes, practically very difficult. You would need to: fix the JavaScript-layer leaks (stealth plugins handle most), provision real GPU access (fixes WebGL), use real Chrome network stacks with correct TLS (mostly fine for Chrome-based automation), and produce realistic behavioral noise at scale. Commercial anti-detect browsers attempt this; our article on anti-detect browser detection covers how sites respond to those tools.


