← back to blog

Headless Chrome and CDP Leaks Explained: How Automation Gets Detected

What headless Chrome actually is

Headless Chrome is just Chrome running without a visible window. Same rendering engine, same JavaScript engine, same network stack. Google added it back in 2017 so developers could run automated tests and scrapers without spinning up a display server. It was never built to hide from anything. It was built to save resources.

That distinction matters because a lot of people assume “headless” is a stealth mode. It isn’t. It’s a deployment mode. The stealth problem showed up later, once bot operators started using headless Chrome (and the tools built on top of it, like Puppeteer, Playwright, and Selenium’s CDP driver) to automate logins, scraping, and account creation at scale. Platforms noticed the traffic pattern and started building detection around it.

Why CDP is the real fingerprint, not “headless” itself

Most automation frameworks don’t talk to Chrome through some special headless API. They talk to it through the Chrome DevTools Protocol, the same protocol that powers the DevTools panel you open with F12. CDP lets an external process open tabs, navigate, read the DOM, intercept network requests, and inject JavaScript, all over a WebSocket connection to the browser.

This is the part operators need to understand: CDP leaves traces that exist independently of whether the browser window is visible. You can run Chrome in full headed mode, with a real window, a real display, and a mouse cursor, and still be driven by CDP. Detection systems that flag “CDP present” catch both. So switching from headless to headed mode alone doesn’t remove the signal. The protocol connection is the tell, not the missing window.

A concrete example: when a CDP client sends Runtime.enable, Chrome starts forwarding JS execution context events to that client. This changes internal browser behavior in ways a page can sometimes detect indirectly, for instance through timing differences or how certain error objects get constructed once the Runtime domain is active. Detection scripts have been built around exactly this kind of side effect. It’s not a bug Google is racing to patch, because from Chrome’s point of view, CDP is working exactly as designed. The “leak” is a byproduct of the protocol doing its job.

The classic JS-level tells

Before protocol-level detection got sophisticated, most bot checks looked at simple JavaScript properties that differ between a real user session and an automated one:

  • navigator.webdriver returning true. This flag exists specifically so automated browsers can be identified, and Selenium in particular sets it by default.
  • Empty or unusual navigator.plugins and navigator.mimeTypes arrays, since a stock automated Chrome profile often has none of the plugin objects a real user’s browser accumulates.
  • Missing window.chrome object or an incomplete chrome.runtime, because some headless configurations don’t initialize it the same way a normal browser session does.
  • Permission query mismatches, where navigator.permissions.query() for notifications returns a different state than what Notification.permission reports, a discrepancy that shows up in some automated setups but not in normal browsing.
  • Function toString() checks. Automation tools sometimes override native functions like navigator.webdriver’s getter. When you call .toString() on a genuinely native function, you get [native code]. A patched or proxied function often reveals itself here, because reproducing that exact native signature is harder than it looks.

None of these are exotic. They’re small inconsistencies that fall out of the fact that a scripted browser environment doesn’t accumulate the same history, extensions, and state a real, aged browser profile does. Any single one of these checks is easy to patch. The problem for automation is that detection scripts stack dozens of them together and score the combination.

Protocol-level leaks that JS patching can’t fix

This is where it gets harder, and where a lot of tooling that claims to “spoof headless detection” falls short. Patching navigator.webdriver to return false or injecting a fake plugins array only addresses what a page can observe through JavaScript running in that page. It does nothing about what the browser exposes at the network and protocol layer.

A few examples:

  • TLS and HTTP/2 fingerprints. The order and values in a Chrome TLS handshake (JA3/JA3S style fingerprints) and HTTP/2 frame settings are consistent for a given Chrome build. If your automation stack pairs a spoofed user agent string with a TLS fingerprint that doesn’t match that browser version, the mismatch is visible to anything inspecting raw traffic, which is exactly what services like Cloudflare and DataDome do before your JavaScript even runs.
  • The CDP WebSocket handshake itself. If a detection script can get code to run in a context that checks for open debugging ports or certain CDP-specific globals, no amount of navigator object patching hides that the tab is attached to a remote debugging session.
  • Timing artifacts from protocol overhead. Every CDP command adds a round trip between your automation script and the browser process. Real user interactions don’t have that round trip. Some detection systems look at the timing distribution of events like clicks or key presses relative to page load, and CDP-driven interaction has a different rhythm than a person using a trackpad.

These aren’t things you fix by editing a JavaScript object before the page loads. They require either not using CDP at all for the sensitive parts of a session, or running a genuinely modified Chromium build where the patches happen below the JavaScript layer, closer to where the protocol and rendering engine actually live.

How anti-detect browsers try to close the gap

This is the actual product category Multilogin, GoLogin, Kameleo, AdsPower, and Dolphin Anty compete in, and it’s worth being precise about what they’re doing. The better-engineered tools in this space don’t just overwrite a handful of navigator properties from injected JavaScript. They work with modified Chromium builds where fingerprint-relevant values (canvas rendering output, WebGL renderer strings, audio context fingerprints, font enumeration, hardware concurrency, screen properties) are adjusted at a lower level, and where each browser profile is isolated with its own cookie jar, storage, and cache so profiles don’t cross-contaminate.

Some also route CDP-based automation through their own internal layer rather than exposing a raw debugging port, specifically to avoid the class of leaks described above. How thoroughly any given tool does this changes between versions and between vendors, and I’m not going to hand out a verdict on a specific product’s current build without a fingerprint test actually run against it. What I will say from running proxy and cloud-phone infrastructure for multi-accounting work is that the gap between “changed a few JS values” and “built a genuinely different fingerprint surface” is large, and it’s the single biggest quality difference between tools in this category.

What detection services actually do with these signals

Platforms like Cloudflare, DataDome, Akamai, and PerimeterX/HUMAN don’t rely on one signal. They combine TLS and HTTP/2 fingerprints, JS-level environment checks, CDP artifacts where reachable, behavioral signals like mouse movement and typing cadence, and account-level signals like IP reputation and session history, then score the whole session. A profile can pass every individual JS check and still get flagged because the TLS fingerprint doesn’t match the claimed browser, or because ten “different” accounts are sharing a fingerprint cluster that’s statistically implausible for ten different people.

This is why isolating each profile properly (separate fingerprint, separate cookie storage, and in serious multi-accounting setups, separate IP through mobile or residential proxies rather than datacenter IPs) matters more than any single anti-detection trick. A clean fingerprint on a flagged datacenter IP still gets caught. A slightly imperfect fingerprint on a clean mobile IP with consistent account history sometimes survives longer than tools alone would suggest, because detection is scored, not binary.

The honest bottom line

No anti-detect browser makes an automated or multi-account session undetectable, and nothing here should be read as a promise that any given tool prevents bans. Detection techniques evolve on both sides. What separates tools that genuinely reduce risk from ones that just paper over the obvious checks is whether the fixes happen at the protocol and browser-engine level or only in injected JavaScript. If you’re evaluating a browser for multi-accounting work, ask specifically how it handles CDP exposure and TLS fingerprint consistency, not just whether navigator.webdriver reads false.

We test these tools against real fingerprinting checks and write up what we actually see, tool by tool, rather than repeating vendor marketing. Start at Anti-Detect Review for the rest of our testing.

Get new guides and videos first — join the Telegram channel.

need infra for this today?