How bot detection works, and what it actually sees
Bot detection is usually described as a black box, which makes it sound like magic and makes it impossible to reason about. It is not a black box. It is three layers of ordinary engineering, and the layer that catches most automation is the most legible one: a script inside the page asking your browser a short list of questions. This page goes through the layers, the questions, and the launch flags that answer them badly.
Three layers, doing three different jobs
The edge. Before a page is served at all, a CDN or WAF scores the connection: IP reputation, ASN, whether the address belongs to a datacentre or a residential range, request rate, TLS handshake characteristics, HTTP/2 settings order, header ordering and casing. None of this involves JavaScript, and none of it is about your browser's configuration — it is about where the request came from and what the connection looked like. A clean browser behind a flagged IP is still a flagged request.
The challenge. If the edge is uncertain, it interposes a page of its own: an interstitial that runs a proof-of-work or a fingerprinting routine, sometimes with a visible widget. The challenge is not really asking you to click a checkbox; it is buying a few hundred milliseconds in which to run the third layer before it decides.
In-page fingerprinting. Once JavaScript is running, the detector interrogates the browser object model directly. This is the layer this page is mostly about, because it is the one automation fails at for reasons that are fixable, and it is the one where a tool's launch configuration decides the outcome before a single request is made.
The vendors, described factually
There is a real market here, and it is worth knowing the shape of it. Cloudflare operates bot management and the Turnstile challenge widget. Other CDNs and edge providers, Fastly and Akamai among them, sell bot-detection products at the edge layer. DataDome and PerimeterX sell dedicated bot-mitigation products. Naming them is useful context for understanding what you are up against, and that is the entire reason they appear on this page.
What the in-page script actually asks
`navigator.webdriver`. The single cheapest check there is. The browser sets this to true when it was launched in a mode that announces automation, and reading it costs one property access. It is the first thing any detector looks at and the first thing any serious stealth effort removes — which is why, on its own, it is no longer sufficient.
CDP leak globals, and `Runtime.enable`. Driving a browser over the Chrome DevTools Protocol is not itself visible to a page, but some of what you do with it is. Enabling the CDP Runtime domain — which is what any operation that evaluates JavaScript in the page context ultimately needs — is observable from inside the page through timing and error-serialisation side effects, and some automation stacks additionally leave identifiable objects on window. Anything that executes script in the page is spending some of this budget; that is a property of the protocol, not of a particular tool, and it is worth knowing rather than being told it has been solved.
`navigator.plugins`. Ordinary desktop Chrome reports a small, specific set of entries. A stub or empty array is an anomaly, and unlike the webdriver flag it is a shape rather than a boolean, so it is harder to fake convincingly than to read.
WebGL vendor and renderer. WEBGL_debug_renderer_info exposes the graphics stack as strings. A browser launched with the GPU disabled reports a software rasteriser, which is both rare among real desktop users and completely unambiguous. This is why --disable-gpu, a flag that exists purely to make headless browsers behave on machines without a GPU, is a detection problem rather than a performance choice.
Canvas fingerprinting. Draw text or a gradient into an offscreen canvas, read the pixels back, hash them. The result depends on the GPU, the driver, the font stack and the rasteriser, so it is stable for a given machine and different across machines. Detectors use it both as an identifier — the same hash appearing from a thousand sessions is informative — and as an anomaly check, because a software renderer produces a distinctly different hash from hardware.
User agent. Not the string on its own, which is trivially set, but its consistency with everything else: the UA claims a platform, and the platform is also observable through navigator.platform, the available fonts, the screen metrics, the timezone and the language list. Contradictions score worse than an unusual-but-coherent browser.
`navigator.languages`. A populated language list is what a real installation carries. An empty list, or a single default, is the kind of small emptiness that automation produces and users do not.
The launch flags that answer those questions badly
Most of the probes above are decided at launch, by flags that were added for the convenience of the harness rather than the browser. Stealthly strips 30+ of them before Chrome starts and reports each removal in spawn_diagnostics.stealth_args_stripped, so the filtering is auditable rather than implicit.
The bottom two rows are the interesting ones. --disable-backgrounding-occluded-windows does nothing a detector cares about functionally; it is a tell purely because Puppeteer sets it by default, so its presence narrows what you are to a much smaller set than 'some browser'. The same is true of --password-store=basic and --use-mock-keychain for Playwright. Signature detection does not need to understand a flag. It only needs to know which tool ships it.
What a cleared challenge looks like
Two recordings, both from the project's own README rather than a reconstruction: the Cloudflare Turnstile bypass shows a challenge being cleared by a browser that was simply started without the tells, and persistent login sessions shows the other half of the problem — a session that is already logged in never has to look like a first-time visitor doing something improbable.
That second one is underrated. A great deal of what detectors score is not 'is this a bot' but 'is this account behaving the way this account behaves'. An automation that starts from a cold profile every run is generating a new anomaly each time before it has done anything at all.
Where to go from here
If you are building automation and losing to detection, the order of operations that follows from the above is: fix the launch flags first, because they are cheap and they decide most of the in-page probes; carry a real profile second, because it removes a whole class of first-visit anomalies; and only then think about the edge layer, which is about where your traffic comes from and is not a browser-configuration problem at all.
- Stealth browser automation — how the flag filtering and profile strategy actually work.
- Does undetectable automation need a kernel driver? — why this is a user-space problem.
- Install the MCP server — the install, the tool surface and the ops CLI.
- Stealthly vs agent browsers — the architecture comparison, with measured numbers.