Selenium's bidirectional protocol and the WebDriver BiDi migration
For most of its life, WebDriver could only speak when spoken to. A client sends an HTTP request, the browser does the thing, the browser sends one response back, and that is the whole conversation. There is no channel for the browser to say, on its own initiative, “a console error just fired” or “a request to this URL is about to go out.” The model is a remote-procedure call with a polling loop bolted on top, and it has been that way since Selenium handed the protocol to the W3C in the early 2010s. Meanwhile the browser itself is the most event-driven thing most of us touch all day.
That mismatch is the reason Puppeteer, Playwright, and Cypress all ended up talking to the browser over something other than WebDriver. WebDriver BiDi is the W3C’s answer to that drift: a standard, cross-browser, bidirectional automation protocol that takes the event-streaming power people went to the Chrome DevTools Protocol for and puts it behind a spec that Firefox, Chrome, and the rest are obligated to implement the same way. Selenium is in the middle of moving its whole implementation onto it, and Firefox has already pulled its CDP support out from under everyone. This post traces how the protocol got here, what it actually is on the wire, and the part that matters most for anyone who studies detection: whether moving from CDP to BiDi changes the tells a browser leaves behind.
The plan is to start with the failure mode of classic WebDriver and how the proprietary protocols filled the gap, then walk the standards history from the 2021 intent-to-prototype through the 2024 production-ready milestone. After that, the protocol mechanics: WebSocket transport, the module layout, and how a BiDi session coexists with a classic one. Then Selenium’s specific migration and the removal of CDP for Firefox. The last third is detection: the navigator.webdriver flag the standard mandates, the cdc_ artifacts ChromeDriver leaves, and the CDP Runtime.enable leak, with a careful look at which of those survive the switch to BiDi and which do not.
The problem classic WebDriver could not solve
The WebDriver protocol that became a W3C Recommendation is a request/response affair carried over HTTP. The client issues commands like “navigate to this URL” or “click this element,” each one a separate HTTP transaction, and the browser answers each in turn. This is clean, easy to reason about, and works across every browser that ships a driver. It is also fundamentally the wrong shape for a lot of what people want to automate.
Consider waiting for a network request, or capturing every console message a page emits, or intercepting a fetch and returning a mock response. None of these fit a command/response model cleanly. The browser produces these things asynchronously, on its own schedule, and a polling client either misses events between polls or hammers the driver with status checks. The W3C’s own explainer for BiDi lists the scenarios classic WebDriver cannot serve well: listening for events, streaming browser logs and errors, intercepting and mocking network traffic, monitoring performance, and tracking dynamic contexts like service workers and iframes as they appear.
Chrome had an answer to all of this already, and it was not WebDriver. The Chrome DevTools Protocol is the same protocol the DevTools front-end uses to talk to the browser, exposed over a WebSocket, and it is bidirectional by design. Enable a domain and the browser starts pushing events at you. Puppeteer was built directly on CDP. Playwright uses CDP for Chromium and patched equivalents elsewhere. Mozilla’s own framing of the situation, in the 2021 intent-to-prototype that James Graham posted to the Mozilla dev-platform list, was blunt about the consequence: tools adopted Chrome’s DevTools Protocol instead of the HTTP-based WebDriver standard, which incentivised writing tests against Chrome only. A proprietary protocol that one vendor controls, becoming the de-facto automation interface for the web, is exactly the outcome a standards body exists to prevent.
So there were two protocols with opposite trade-offs. WebDriver Classic had cross-browser reach and a W3C spec, but a synchronous transport that did not match the browser. CDP had the bidirectional event model everyone wanted, but it was Chrome-only and changed whenever the Chrome team wanted it to. The W3C did not pick one. It set out to build a third thing.
*WebDriver BiDi was designed to occupy the bottom-right corner that neither existing protocol could reach.*2019 to 2024: the standards history
The work sits inside the W3C Browser Testing and Tools Working Group, the same group that owns classic WebDriver. The group began sketching a bidirectional protocol in 2019, and the effort moved into a concrete shape over the following two years. The membership reads like a vendor summit: Google, Mozilla, Apple, Microsoft, and Salesforce all have people in the room, which is the only way a cross-browser automation standard can actually bind anyone.
Mozilla’s public commitment came on 3 June 2021, when James Graham filed the intent to prototype WebDriver-BiDi in Firefox. The stated motivation was the one already described: claw the automation ecosystem back from a single-vendor protocol and onto a standard, because a command/response protocol over HTTP does not match the highly asynchronous nature of modern browser engines. From Mozilla’s side this was a four-year project. By the time Firefox shipped production-ready support, the company had been helping shape the spec and gradually implementing it for over four years, first in the design and then in the code.
The first implementations landed in 2022. Chrome and ChromeDriver 106 shipped initial BiDi support that year, and Firefox 102 did the same. These early builds were narrow. The 2023 status update from the Chrome team, written by Mathias Bynens and published on 9 May 2023, was largely about one module: logging. The headline capability was being able to verify that a page loads without any console logs, warnings, or errors, powered by BiDi’s log events rather than CDP. Puppeteer’s BiDi support at that point was explicitly experimental.
The milestone that matters landed on 7 August 2024. Matthias Rohmer’s post for the Chrome team announced that Firefox 129 and Puppeteer 23 each got production-ready WebDriver BiDi support, with the pitch that you could now automate Firefox with the same concise API as Chrome. That is the line in the sand. Before it, BiDi was a promising draft with partial coverage. After it, a real automation tool could drive two different browser engines through one standard protocol and reach the network, script, and logging features that previously required CDP. The same announcement carried the other shoe: CDP support in Firefox was deprecated from Firefox 129 and scheduled for removal at the end of 2024.
As of mid-2026 the specification is still a W3C Working Draft on the Recommendation track, not yet a final Recommendation. The published draft carries a date in June 2026, which is the point: BiDi is a living standard that keeps absorbing new modules rather than freezing. That status surprises people who assume “production-ready in shipping browsers” implies “finished spec.” The two have decoupled. Browsers ship the stable parts; the working group keeps adding to the draft.
*From the working group's first sketches to the August 2024 production-ready milestone. The orange node is where two engines first spoke one protocol.*What BiDi actually is on the wire
The transport is a WebSocket. A BiDi client opens a WebSocket connection to the browser (or to the driver acting on its behalf) and from then on both sides can send messages whenever they like. Commands flow client-to-browser, responses flow back keyed to the command that triggered them, and events flow browser-to-client unprompted once the client has subscribed to them. The spec describes the message encoding as similar to JSON-RPC, with a deliberate caveat: it does not normatively reference JSON-RPC. The messages are JSON objects with the familiar shape of a method name and a parameters block, but BiDi defines its own framing rather than inheriting another spec’s rules.
The standard way the spec puts it is that it extends WebDriver by introducing bidirectional communication, so that instead of a strict command/response format, events can stream from the user agent to the controlling software, better matching the evented nature of the browser DOM. The word “extends” is doing real work there. BiDi is not a clean-sheet replacement that throws WebDriver Classic away. It is built as a layer that can coexist with the classic HTTP endpoint, which is why Selenium can migrate gradually instead of forcing a rewrite.
The protocol is organised into modules, each a group of related commands and events for one aspect of the browser. The current draft defines, among others:
session: status, creating and ending sessions, and the subscribe/unsubscribe machinery that turns event streams on and off.browser: window and user-context management at the browser level.browsingContext: navigation, screenshots, and handling user prompts; a browsing context is roughly a tab or frame.script: evaluating code in a realm, and the execution-context plumbing that goes with it.network: observing requests, intercepting them, and collecting response data.storage: reading and setting cookies.log: collecting console and JavaScript log entries.input: synthesising user actions.emulation: device and environment simulation.webExtension: installing and managing extensions.
Two design choices are worth pulling out because they shape everything downstream. The first is the subscription model. Events do not stream by default; a client calls session.subscribe for the event types it cares about, which keeps the channel quiet until someone asks for noise. The second is efficiency relative to CDP. The Chrome team noted that retrieving an object’s id and value takes one round trip in BiDi where CDP needs two, because the protocol was designed with the lessons of CDP’s chattiness in hand rather than accreted over years of DevTools needs.
There is one more structural point that the explainer is careful about. BiDi is meant to be easily mappable to and from native devtools protocols. The working group did not standardise CDP, because, as the Chrome team put it, CDP’s Chrome- and DevTools-specific elements cannot be directly replicated in the BiDi spec, and implementing CDP as-is would be infeasible for other browsers. Instead BiDi defines a vendor-neutral surface that each browser can wire to its own internals. In Chromium that internal layer is still CDP under the hood; in Firefox it is the Remote Agent and Marionette. The client sees one protocol regardless.
Selenium’s migration
Selenium handed WebDriver to the W3C, so it is fitting that Selenium is now rebuilding itself around the W3C’s bidirectional successor. The project’s own documentation states the direction plainly: it is updating its entire implementation from WebDriver Classic to WebDriver BiDi, while keeping backwards compatibility as much as possible. The hedge matters. Selenium has an enormous installed base of test suites written against the classic API, and none of those break on day one. The classic commands keep working; BiDi adds the event-driven capabilities alongside them.
For a few years Selenium 4 shipped a CDP bridge. If you wanted to listen for console logs or intercept network traffic in Chrome, Selenium let you reach into CDP directly through a versioned binding. This was always framed as a stopgap. The documentation has said for a while that CDP support is temporary until WebDriver BiDi has been implemented, and the current Selenium BiDi surface covers the modules that the CDP bridge used to cover: logging, network, script, and browsing context, exposed through a W3C-compliant API rather than a Chrome-specific one.
The cleanest illustration of the migration is what happened to CDP for Firefox. Selenium’s CDP support for Firefox was always partial. It never reached feature parity with the Chrome bridge, because Firefox’s own CDP implementation was a subset. Henrik Skupin’s post on the Firefox developer-experience blog, dated 28 May 2024, gives the number: Firefox’s CDP implementation covered 82 APIs, a subset of the full CDP surface, when it was enabled by default back in February 2021. Firefox then put that implementation into limited maintenance mode, adding no new features, and moved its investment to BiDi, which already offered a broader range of features than the experimental CDP implementation, including preload scripts, network interception, and richer logging.
Selenium followed Firefox out the door. Puja Jagani’s post on the Selenium blog, dated 3 February 2025, announced that CDP support for Firefox was deprecated in Selenium 4.27 and 4.28 and fully removed in 4.29.0. The reasoning was the partial implementation, Firefox’s own move to BiDi, and the broader push toward standardised, browser-agnostic automation. By the time of removal a major portion of WebDriver BiDi functionality was available across all Selenium language bindings, so the CDP bridge for Firefox had nothing left to offer that BiDi could not. Firefox 129 and later do not enable CDP by default.
For Chromium-based browsers the picture is more layered. Chrome still speaks CDP, because DevTools needs it and because BiDi in Chromium is implemented on top of CDP internally. So Selenium against Chrome can still use the CDP bridge for Chrome-specific features that BiDi has not yet absorbed. The 2026 pattern that has settled in practice is to use BiDi for anything cross-browser (console, network, basic script evaluation) and fall back to CDP only for Chrome-specific advanced features that the standard has not reached. The direction of travel is one-way. Each Selenium release that lands another BiDi module shrinks the set of reasons to touch CDP at all.
What the switch changes for detection
This is the part that matters to anyone on the detection side, and the honest answer has three layers. Some tells are protocol-independent and survive the switch untouched. Some are CDP-specific and genuinely go away when a tool stops using CDP. And the new BiDi channel introduces its own surface, much of which is not yet publicly characterised. Take them in order.
The flag the standard mandates
The most basic automation tell is required by the standard itself. The WebDriver specification defines a webdriver-active flag, initially false, set to true when the user agent is under remote control, and the navigator.webdriver property returns the value of that flag. The whole point of the property, in the spec’s words, is to give co-operating user agents a standard way to inform the document that it is controlled by WebDriver, so a page can run alternate code paths during automation. It exists to be detected. That is its job.
Crucially, this is bound to the WebDriver session, not to any particular protocol. Mozilla’s tracking bug on the subject is explicit that the navigator.webdriver active flag has to be bound to the WebDriver (Marionette, BiDi) session lifetime. A BiDi session sets the flag exactly the same way a classic session does. So moving from CDP to BiDi does nothing to hide navigator.webdriver. A site that checks one boolean still catches a default BiDi-driven browser. People who want to mask this have historically launched Chrome with flags that suppress the property, but that is a launch-time hack independent of the automation protocol, and it has its own side effects that detectors look for. The standard’s position is that an honest automation session announces itself, and BiDi keeps that promise.
The CDP-specific tells that actually go away
ChromeDriver leaves fingerprints that have nothing to do with the wire protocol per se and everything to do with how ChromeDriver injects its helper code. The well-documented ones are a family of global variables with a cdc_ prefix. Castle’s analysis from July 2025 lists them: properties like cdc_adoQpoasnfa76pfcZLmcfl_Array, cdc_adoQpoasnfa76pfcZLmcfl_Promise, cdc_adoQpoasnfa76pfcZLmcfl_Object, plus Proxy, Symbol, JSON, and Window variants, along with the presence of window.ret_nodes. A page that enumerates the window object and finds any of these has caught ChromeDriver specifically. These are an artifact of the driver, not of CDP or BiDi, so whether they survive depends on whether the driver still injects them, not on which protocol carries the commands.
The tell that genuinely changes is the CDP serialization leak, and it is worth getting exactly right because it is widely misunderstood. The mechanism: an automation client that uses CDP enables the Runtime domain by sending Runtime.enable, because that is how it discovers the execution-context ids it needs to run scripts in page frames. Without those ids a tool cannot call the equivalent of Page.evaluate, and one estimate is that the large majority of automated actions route through page evaluation in some form. Enabling the Runtime domain has a side effect: the browser starts firing Runtime.consoleAPICalled events, and to deliver those events it serializes logged objects across the CDP channel.
The detection exploited that serialization. A page defines a getter on the stack property of an Error object, logs the error with something like console.debug, and waits. If CDP’s Runtime domain is enabled, serializing the logged error reads the stack getter and the getter’s side effect fires, revealing automation. The technique was reliable precisely because it survived the obvious countermeasures: it caught bots that had removed navigator.webdriver and spoofed their user agent. Antoine Vastel publicised it in June 2024, though anti-bot vendors had reportedly used it quietly for a couple of years before that.
Two separate developments dismantled this signal, and it helps to keep them apart. The first is a V8 change, not a protocol change. In May 2025 the V8 team landed commits that stop error previews from running user-defined getters. The CDP signal stopped working everywhere as a result, regardless of automation tool, because the getter the detection relied on never gets called during serialization any more. Castle documented the two commits, dated 7 and 9 May 2025, with a new internal guard that refuses to call a getter that has a script id indicating user code. That fix is independent of BiDi. It made the trick stop working even for CDP-based tools.
The second development is the protocol switch itself, and this is the part directly relevant to the migration. A BiDi-only tool does not send Runtime.enable over CDP, because it is not speaking CDP. BiDi has its own script module with its own way to get execution contexts and evaluate code, carried over the BiDi WebSocket. The whole premise of the classic detection, an enabled CDP Runtime domain producing serialization side effects, simply does not apply to a session that never enables that domain. So a tool that has fully migrated to BiDi sidesteps the entire class of CDP-Runtime-domain tells, not because BiDi is stealthy by design, but because it bypasses the specific machinery the detection watched.
That second point comes with a sharp caveat, and it is where honesty matters more than a clean story. In Chromium, BiDi is implemented on top of CDP internally. The browser-side mapping layer may still drive CDP domains under the hood to satisfy a BiDi command. Whether a given BiDi operation triggers the same observable serialization side effect as a direct Runtime.enable is an implementation detail of the Chromium mapping, and the exact behaviour is not something the W3C spec pins down. The precise internal field layout and which CDP domains the Chromium BiDi backend enables for a given BiDi subscription are not publicly documented in full; what can be said with confidence is that the client-visible protocol is BiDi and that a tool no longer issues Runtime.enable itself. Anyone claiming BiDi is undetectable on this axis is overreaching. The correct claim is narrower: the specific page-observable signal that depended on a tool enabling CDP’s Runtime domain does not fire when the tool drives the browser through BiDi instead, and the V8 patch retired that signal for CDP users too.
The new surface BiDi brings
A new protocol is a new attack surface for detection, not the end of it. BiDi opens its own WebSocket, and the browser running a BiDi session has the webdriver-active flag set, which is itself the cleanest tell. Beyond that the public detection literature has mostly studied CDP, because CDP is what Puppeteer and Selenium have leaned on for years. The equivalent characterisation of BiDi’s page-observable side effects is thinner. DataDome’s own published position is that detection vendors are preparing for how BiDi could change the picture and intend to adapt, which is vendor-speak for “we have not finished mapping it yet but we will.” Treat any specific claim about a BiDi-only tell that is not backed by a reproducible test as inference rather than fact. The honest state of the art in 2026 is that the protocol migration retires some well-known CDP signals, the mandated navigator.webdriver flag survives it, and the new BiDi-specific surface is still being charted in public.
If you want the broader detection context that surrounds this, the Runtime.enable leak and the V8 patch war covers the CDP signal in depth, the Chrome DevTools Protocol as a detection vector covers the wider CDP surface, and the detection surface of each driver compares how Playwright, Puppeteer, and Selenium each expose themselves.
Where this leaves things
The WebDriver BiDi migration is two stories wearing one name. The first is a standards story with a clean arc: the W3C watched the automation world drift onto a single vendor’s proprietary protocol, spent five years building a vendor-neutral bidirectional alternative, and got it production-ready across two browser engines by August 2024. Selenium, which started the whole lineage, is rebuilding on top of it. Firefox has already removed the CDP crutch. That part is close to settled, and the remaining work is filling in modules rather than proving the concept.
The second story, the detection one, is messier and less finished, and it resists the tidy summary people want. Moving from CDP to BiDi is not an evasion technique. The standard deliberately keeps the navigator.webdriver flag that announces automation, and ChromeDriver’s cdc_ artifacts are a property of the driver rather than the wire protocol. What the switch genuinely changes is narrow and specific: a BiDi-only tool stops sending CDP’s Runtime.enable, so the serialization side-effect signal that watched for an enabled CDP Runtime domain has nothing to latch onto, and that same signal was independently retired for CDP users by a V8 change in May 2025. Whatever Chromium’s internal mapping does underneath does not change the client-visible protocol, and the public characterisation of BiDi’s own side effects is still incomplete.
The thing worth holding onto is that a standard automation protocol and a stealthy one are different design goals, and BiDi was built for the first. It announces itself by spec. The reason a BiDi session looks less like a known bot than a CDP session is not that anyone hid the controls; it is that an old, well-studied leak was tied to a CDP domain that BiDi sessions no longer enable. Detection vendors know exactly where to look next, because the flag that says “this browser is under remote control” is still right there in the standard, returning true on request.
Sources & further reading
- W3C Browser Testing and Tools Working Group (2026), WebDriver BiDi — the Working Draft defining the WebSocket transport, the JSON message shape, and the module layout.
- w3c/webdriver-bidi (n.d.), Explainer — the design rationale for a new protocol, why CDP was not standardised directly, and the coexistence goals with classic WebDriver.
- James Graham, Mozilla (2021), Intent to Prototype: WebDriver-BiDi — the 3 June 2021 post laying out Mozilla’s motivation to pull automation back onto a standard.
- Jecelyn Yeen and Maksim Sadym, Chrome (2021), WebDriver BiDi: the future of cross-browser automation — the original Chrome explainer on combining the best of WebDriver Classic and CDP.
- Mathias Bynens, Chrome (2023), WebDriver BiDi: 2023 status update — the log-module milestone and the 2022 Chrome 106 / Firefox 102 first-ship versions.
- Matthias Rohmer, Chrome (2024), WebDriver BiDi production-ready in Firefox, Chrome and Puppeteer — the 7 August 2024 announcement of Firefox 129 and Puppeteer 23 production support.
- Henrik Skupin, Mozilla (2024), Deprecating CDP support in Firefox: embracing the future with WebDriver BiDi — the 82-API CDP subset, limited-maintenance mode, and the Firefox 129 deprecation.
- Puja Jagani, Selenium (2025), Removing Chrome DevTools support for Firefox — the deprecation in Selenium 4.27/4.28 and removal in 4.29.0.
- Selenium project (n.d.), BiDirectional functionality — the project’s statement that CDP support is temporary and the current BiDi feature coverage.
- Antoine Vastel, Castle (2025), How to detect Selenium bots — the
cdc_global variables,navigator.webdriver, and the CDP serialization side effect. - Antoine Vastel, Castle (2025), Why a classic CDP bot detection signal suddenly stopped working — the
Runtime.enable/consoleAPICalledmechanism and the May 2025 V8 commits that disabled it. - Rebrowser (2024), How to fix Runtime.Enable CDP detection — why automation libraries enable the Runtime domain and what the leak depends on.
- MDN (n.d.), Navigator: webdriver property — the spec definition of the property and the conditions under which it returns true.
Further reading
The history of Selenium and WebDriver: from 2004 to the W3C standard
Traces Selenium from Jason Huggins's 2004 JavaScriptTestRunner through Selenium RC's proxy hack, the 2009 WebDriver merger, and WebDriver becoming a W3C Recommendation in 2018.
·22 min readKasada's anti-instrumentation: how it detects CDP, Playwright, and patched runtimes
A reference on Kasada's automation-detection layer: how it spots Chrome DevTools Protocol instrumentation, Playwright and Puppeteer artifacts, and patched stealth runtimes that lie about themselves.
·17 min readHow anti-bot systems fingerprint the JavaScript runtime
A reference on the JS-runtime fingerprinting surface: error stack formats, Function.prototype.toString, feature and timing probes, property enumeration order, and the engine quirks that betray a patched or automated browser.
·21 min read