WebDriver BiDi is a W3C standard protocol for browser automation in which a client and a browser exchange JSON messages over a WebSocket in both directions, so the client can send commands and the browser can push events, in the same way for every browser that implements it.
The name means "bidirectional WebDriver". It adds the event stream of the Chrome DevTools Protocol to the cross-browser standard of classic WebDriver. This entry is part of the screenshot glossary.
What it means in plain terms
Browser automation used to have two camps:
- Classic WebDriver (the protocol behind Selenium) is a W3C standard. Every major browser supports it. It is request and response over HTTP: you ask, the driver answers. The browser cannot tell you that something happened.
- CDP is Chrome's own debugging protocol. It runs over a WebSocket and streams events: console messages, network requests, page loads. Tools such as Puppeteer are built on it. It is not a standard, and it changes with Chrome.
WebDriver BiDi takes the transport and event model of CDP and puts it into a vendor-neutral specification. Chrome, Firefox and Safari's WebDriver work in the same W3C group, and Mozilla has moved Firefox automation to BiDi.
How the three protocols compare
flowchart LR subgraph Classic["Classic WebDriver"] C1[Client] -- "HTTP request" --> D1[Driver] -- "HTTP response" --> C1 end subgraph CDP["Chrome DevTools Protocol"] C2[Client] <-- "WebSocket: commands + events" --> B2[Chrome only] end subgraph BiDi["WebDriver BiDi"] C3[Client] <-- "WebSocket: commands + events" --> B3[Chrome, Firefox, ...] end
| Classic WebDriver | CDP | WebDriver BiDi | |
|---|---|---|---|
| Standard | W3C | No (Chrome project) | W3C |
| Transport | HTTP | WebSocket or pipe | WebSocket |
| Browser can push events | No | Yes | Yes |
| Browsers | All major | Chromium-based | Chrome, Firefox; others in progress |
| Screenshot command | GET /session/{id}/screenshot |
Page.captureScreenshot |
browsingContext.captureScreenshot |
What a BiDi session looks like on the wire
To see the protocol without a library, we started chromedriver, created a classic session with the capability webSocketUrl: true, and sent BiDi commands over the WebSocket URL that the driver returned. These are the exact frames our client sent (the context ID is the tab):
{"id":1,"method":"browsingContext.getTree","params":{}} {"id":2,"method":"browsingContext.setViewport","params":{"context":"D7AF712EEC2C0BE30F2EDD90129DA329","viewport":{"width":1280,"height":800}}} {"id":3,"method":"browsingContext.navigate","params":{"context":"D7AF712EEC2C0BE30F2EDD90129DA329","url":"https://example.com","wait":"complete"}} {"id":4,"method":"browsingContext.captureScreenshot","params":{"context":"D7AF712EEC2C0BE30F2EDD90129DA329","origin":"viewport"}}
Each command carries an id. The browser answers with a message that has the same id and a result, or type: "error". Messages without an id are events, for example browsingContext.load. The screenshot result is base64 PNG data. The full 50-line client, using only Node's built-in fetch and WebSocket, produced a 1280×800 PNG.
Measured: CDP vs BiDi for the same capture
We captured https://example.com at 1280×800 five times on each protocol with Puppeteer, with a new browser each run.
| Protocol | Median launch | Median page.screenshot() |
Output |
|---|---|---|---|
| CDP (default) | 351 ms | 81 ms | 81,234 bytes |
| WebDriver BiDi | 371 ms | 77 ms | 81,234 bytes, same SHA-256 |
Method: 2026-10-15, Apple M1 Pro, Node 22.18.0, [email protected], Chrome for Testing 155.0.8059.79, 5 runs per protocol. The differences are within run-to-run noise. The point of the table is the last column: the pixels were identical. Choose the protocol for portability and features, not for speed.
How to use it
Puppeteer
Puppeteer speaks CDP to Chrome by default. One launch option switches it to BiDi:
import puppeteer from 'puppeteer'; // protocol: 'webDriverBiDi' switches Puppeteer from CDP to WebDriver BiDi. const browser = await puppeteer.launch({ protocol: 'webDriverBiDi' }); try { const page = await browser.newPage(); await page.setViewport({ width: 1280, height: 800 }); await page.goto('https://example.com', { waitUntil: 'load' }); await page.screenshot({ path: 'bidi.png' }); console.log(await browser.version(), 'protocol:', browser.protocol); } finally { await browser.close(); }
With Firefox (browser: 'firefox'), Puppeteer uses BiDi without this option. With Chrome, page.createCDPSession() still worked in BiDi mode in our test, because Chrome serves BiDi on top of its CDP layer. In Firefox there is no CDP to fall back to, so code that needs raw CDP is Chrome-only.
Selenium
Selenium 4 enables BiDi with a capability, then exposes it as modules:
import { writeFile } from 'node:fs/promises'; import { Builder } from 'selenium-webdriver'; import chrome from 'selenium-webdriver/chrome.js'; import BrowsingContext from 'selenium-webdriver/bidi/browsingContext.js'; const options = new chrome.Options().addArguments('--headless=new', '--window-size=1280,800').enableBidi(); const driver = await new Builder().forBrowser('chrome').setChromeOptions(options).build(); try { const context = await BrowsingContext(driver, { browsingContextId: await driver.getWindowHandle() }); await context.navigate('https://example.com', 'complete'); const base64 = await context.captureScreenshot(); await writeFile('selenium-bidi.png', Buffer.from(base64, 'base64')); } finally { await driver.quit(); }
Note that --window-size sets the window, not the viewport. Our capture came out 1280×657. Use browsingContext.setViewport when the size matters.
Playwright
Playwright 1.64 has no public switch to BiDi. It drives Chromium over CDP, and Firefox and WebKit over its own protocol with patched browser builds. If your stack must use BiDi, use Puppeteer or Selenium.
RenderScreenshot
The protocol is an internal detail of the renderer, so there is nothing to set. A request names the page and the output, and the API returns the image:
curl "https://api.renderscreenshot.com/v1/screenshot?url=https%3A%2F%2Fexample.com&width=1280&height=800" \ -H "Authorization: Bearer $RS_API_KEY" -o example.png
Tested on 2026-10-15 with Node 22.18.0, [email protected], [email protected] and chromedriver 155.0.8059.79. The curl request was not run for this page.
Common mistakes
- Expecting BiDi to change the pixels. It changes how you talk to the browser, not how the page renders.
- Mixing CDP calls into code that should be portable. Raw CDP calls can work with Chrome in BiDi mode and then fail in Firefox. Keep them behind a check of the browser you run.
- Forgetting
webSocketUrl: true. Without it, a Selenium or raw WebDriver session has no BiDi endpoint. - Treating the spec as finished. Some modules are still being filled in by browsers. Check that each command you need works in each browser you target.
Related terms
For a tool-level choice, read Puppeteer vs Selenium. To skip the protocol layer, create a free account.