How sites actually spot a headless browser

Short answer: Sites spot headless browsers through the traces automation leaves: navigator.webdriver set to true, an empty plugin list, a user agent that says HeadlessChrome, and a WebGL renderer running on SwiftShader instead of a real GPU. Any one of those flags a session. Together with behaviour and network signals, they are enough to score a visitor as a bot.

The four passive tells

The most direct signal is navigator.webdriver. The W3C WebDriver specification requires any browser under automation to expose this property as true. In a real desktop browser it returns false or is undefined. One line of JavaScript reads it, and the answer is nearly always decisive for vanilla automation.

The second tell is the plugin list. A real desktop Chrome reports plugins. Headless Chrome reports none: navigator.plugins returns an empty list with length zero, which no real desktop installation reports. It is a small detail, and it is one of the most reliable, because there is no legitimate reason for a desktop browser to have no plugins at all.

The third is the user agent. A default headless Chrome user agent literally contains the string HeadlessChrome. Operators patch it, of course, but the patch is only the first layer, and the layers underneath still leak. The fourth is WebGL: headless configurations often render through SwiftShader, a software renderer, so the UNMASKED_RENDERER_WEBGL string names Google SwiftShader instead of a real GPU model. A canvas drawn in SwiftShader also hashes differently from the same canvas drawn on hardware, which makes it a reliable automation detector on its own.

Why patching one signal never works

Stealth plugins exist, and they patch dozens of JavaScript-visible leaks: they override navigator.webdriver, fake the plugin list, spoof the user agent, and smooth the canvas hash. Against a casual detector they work. Against a serious one they do not, because the patch itself leaves traces.

A badly patched navigator.webdriver is often more suspicious than an unpatched true. If the property descriptor looks non-standard, or the getter function can be serialised, or Object.getOwnPropertyDescriptor returns something different from what a real browser exposes, a careful detector notices the patch. The same applies to the other signals: each one patched correctly is possible, but patching all of them consistently, with no contradiction between them, is the hard part.

And the JavaScript layer is only one of three. Below it sit the network signals: TLS fingerprints that do not match the claimed Chrome version, HTTP/2 frame ordering that differs from a real browser, and IP reputation. A request that claims to be Chrome on Windows but renders WebGL through SwiftShader, has an empty plugin list, and arrives from a datacenter IP is not a person with an unusual setup. It is a contradiction, and modern anti-bot systems score consistency, not any one flag.

Behaviour is the layer automation cannot fake cheaply

Even with a perfect fingerprint, a bot has to behave like a person, and behaviour is where the cost goes up. Real users move the mouse in curves with variable speed, scroll unevenly, pause to read, and mistype. Bots move in straight lines, click instantly, scroll at constant speeds, and fill forms at machine pace. Mouse heuristics, timing, and dwell time are the signals that survive when the fingerprint is patched.

That is why the serious bot operators do not rely on a single trick. They run real browsers on residential IPs, add randomised delays, and simulate human input patterns. The cost per request goes up by an order of magnitude, and the operation stops being a cheap script and becomes a distributed system that needs maintaining. Detection does not have to be perfect. It has to make the attack more expensive than the data is worth.

For the defender, the practical lesson is to score, not to block on one signal. A single navigator.webdriver check blocks the lazy bots and annoys nobody real. A layered score, browser signals plus network signals plus behaviour, with a challenge for the grey zone instead of a hard block, catches the serious automation without punishing the person on an unusual setup.

Keep reading: Why do bots love your search bar more than your homepage?, Credential stuffing: why a 0.1 percent success rate is a disaster, and Why bots read your robots.txt before they attack cover the nearby cases in this series.

Frequently asked questions

What is a headless browser?

A headless browser is a web browser run without a visible window, controlled by code instead of a person. Tools like Playwright, Puppeteer, and Selenium drive it through the Chrome DevTools Protocol. It is used for testing, monitoring, and scraping, and for credential stuffing, scalping, and click fraud.

Can a website detect Playwright or Puppeteer?

Yes. Vanilla Playwright and Puppeteer leak several signals: navigator.webdriver is true, the plugin list is empty, the user agent contains HeadlessChrome, and WebGL often reports SwiftShader. Stealth plugins patch many of these, but the patches leave their own traces and do not fix network-layer signals like TLS fingerprints.

Is navigator.webdriver enough to block bots on its own?

It catches the lazy automation, which is a large share of it, but it is not enough alone. Serious operators patch it. The robust approach scores many signals together, browser, network, and behaviour, and challenges the grey zone instead of blocking on one flag.

Why do anti-bot systems score consistency instead of single flags?

Because any one signal can be spoofed, and any one signal can appear on a real user with an unusual setup. A request that claims to be Chrome on Windows but renders through SwiftShader, has no plugins, and arrives from a datacenter IP contradicts itself. Contradictions are much harder to fake than individual values.

See your own numbers.

A free bot-traffic audit shows the human-automated split in your live traffic - no code changes, no commitment.

Get a free bot-traffic audit