How sites detect Puppeteer, Playwright, and Selenium through Chrome DevTools Protocol side effects like the Runtime.enable serialization leak.
Puppeteer, Playwright, and much of modern Selenium all drive Chrome through the same low-level channel: the Chrome DevTools Protocol (CDP), the interface that powers Chrome DevTools itself. That shared foundation is convenient for automation authors, but it also means every one of these tools can leave the same family of tells — independent of whatever the user-agent string or navigator.webdriver claim. This guide explains what CDP is, the specific side effects it creates, and how detection systems turn those side effects into a signal.
Key Takeaways
- CDP is the shared control channel behind Puppeteer, Playwright's Chromium driver, and CDP-based Selenium sessions — a detector that spots CDP itself catches all three at once.
- Enabling the Runtime domain has an observable side effect: once a client sends
Runtime.enable, everyconsole.*call gets intercepted and its arguments serialized for a preview, which eagerly triggers property getters a page would otherwise never touch. - That serialization side effect is a detection primitive: a page can define a getter on a value passed to
console.logand check whether it fires immediately, revealing an attached CDP client without ever showing a visible log. - Frameworks differ in exposure: Puppeteer and Playwright's Chromium mode enable
Runtime.enableby default to track execution contexts; patched builds avoid it by managing context IDs a different way. - This is one signal in a layered stack, not a silver bullet — it complements the
navigator.webdriverand rendering-anomaly checks covered in our headless browser detection guide.
What CDP Is and Why Automation Depends On It
The Chrome DevTools Protocol is the JSON-RPC interface that lets an external client inspect and control a running Chromium instance: evaluate JavaScript, intercept network requests, capture screenshots, and receive console and exception events. It's the same protocol DevTools itself speaks to the browser when you open it manually.
Puppeteer and Playwright's Chromium backend connect over CDP directly. Selenium traditionally used the W3C WebDriver wire protocol, but Chromium's WebDriver implementation (chromedriver) is itself a CDP client under the hood, and Selenium 4's Chromium-specific DevTools API exposes CDP directly. Because all three ultimately share this one control surface, a detector that targets CDP mechanics — rather than a framework-specific artifact — has a chance of catching all of them with a single check.
CDP Signals a Page Can Observe
A web page has no direct API to ask "is a CDP client attached?" CDP is designed to be invisible to page JavaScript. But attaching a client changes what happens inside the renderer in ways a page can indirectly measure: certain protocol domains only emit events once a client asks for them, and servicing those events runs extra code on the same JavaScript thread the page is executing on. The most useful of these turns out to be enabling the Runtime domain.
Runtime.enable and the Console-Serialization Leak
Automation clients send Runtime.enable to receive execution-context and console-API-call events — Puppeteer and Playwright need this to know which context ID to evaluate scripts in. Once that domain is active, the V8 inspector intercepts every console.* call before it reaches the visible console and builds an object preview for the corresponding CDP event, whether or not a human ever opens DevTools to see it.
Building that preview means walking the arguments' properties, and walking properties means invoking their getters. In an uninstrumented page, a getter on an object logged via console.log only fires if a human actually has DevTools open and looking. With Runtime.enable active, it fires unconditionally, immediately, as a side effect of the CDP event pipeline itself:
// Runs immediately (before any output appears anywhere) only when a
// CDP client has the Runtime domain enabled.
let cdpDetected = false;
const probe = {};
Object.defineProperty(probe, 'flag', {
get() {
cdpDetected = true;
return 'x';
},
});
console.log(probe);
// Check cdpDetected on the next tick — no visible console needed.
This is a timing/serialization side channel, not a documented API: it depends on how V8's inspector builds console previews, which is an implementation detail rather than a stable contract. That is also why it hasn't been a permanent tell — engine-level behavior around this exact interception has shifted over time as Chromium changes how the inspector processes console arguments.
Puppeteer & Playwright Footprints
Both frameworks enable the Runtime domain in their default configuration, which is what makes the technique broadly effective against out-of-the-box scripts rather than a rare edge case.
| Tool | Default CDP behavior | Common mitigation |
|---|---|---|
| Puppeteer | Sends Runtime.enable to track execution contexts | Community forks avoid keeping the domain enabled, recovering context IDs a different way |
| Playwright (Chromium) | Enables Runtime per execution context for its default page world | Community patches run automation scripts in isolated worlds obtained without Runtime.enable |
Selenium (chromedriver / W3C) | Primarily uses the W3C WebDriver wire protocol; CDP usage is narrower and opt-in via the DevTools API | Avoiding the DevTools API sidesteps this specific check entirely |
The practical result: a detector built around this one mechanism used to separate default Puppeteer/Playwright sessions from plain WebDriver-based Selenium sessions, until automation authors adjusted how their tools manage CDP.
Detection Heuristics vs. Evasion
This is the same cat-and-mouse dynamic covered in our bot detection techniques guide, playing out one level lower in the stack. Once the Runtime.enable side channel became known, it spread quickly through bot-management vendors because it caught multiple frameworks with one check and didn't depend on the JavaScript-layer patches that stealth plugins already covered. Automation maintainers responded with builds that skip Runtime.enable or restructure how they track execution contexts, closing that specific hole while keeping the rest of the framework's behavior unchanged — the same pattern visible in a long-running Puppeteer GitHub issue tracking automation-detection reports against the library over several years.
Neither side stays ahead for long. A signal that depends on undocumented inspector internals is exactly the kind of thing an engine change can quietly break or restore, which is why serious detection stacks treat it as one input in a larger score — alongside navigator.webdriver, WebGL rendering anomalies, and the network-layer fingerprints described elsewhere on this site — rather than a standalone verdict.
Testing Your Own Automation Setup
If you maintain CI browser tests, accessibility audits, or monitoring scripts, it's worth knowing what a detection system sees about your sessions. BrowserInsight's bot detection tool surfaces navigator.webdriver, headless artifacts, and other automation signals your browser currently exposes, and the kernel check confirms which rendering engine is actually running underneath. Neither tool inspects CDP protocol traffic directly — that inspection happens server-side — but they show the adjacent signals a real detection stack would combine with it.
Frequently Asked Questions
Can a website directly detect that CDP is attached?
Not through a documented API — CDP is designed to be invisible to page JavaScript. What a page can detect are side effects of specific CDP domains being active, such as the console-serialization behavior that appears when the Runtime domain is enabled.
Does this affect Selenium the same way as Puppeteer and Playwright?
Less consistently. Selenium's classic path runs over the W3C WebDriver wire protocol rather than CDP directly, so it doesn't automatically trigger CDP-domain side effects the way Puppeteer and Playwright's default Chromium configurations do. Selenium sessions that explicitly use Chromium's DevTools API can still trigger the same checks.
Is enabling Runtime.enable a mistake automation authors made?
No — it's the straightforward way to get execution-context and console events over CDP, and it's exactly what DevTools itself does when a human opens it. The side effect only becomes a bot signal because a script, unlike a human, has no other reason for a CDP client to be attached in production.
Does fixing this one signal make automation undetectable?
No. It removes one input from a detection score that also includes rendering inconsistencies, behavioral analysis, and network-layer fingerprinting — see our guides on headless browser detection and bot detection techniques for the rest of that stack.


