WebDriver BiDi: The Cross-Browser Automation Protocol

WebDriver BiDi is the W3C two-way browser automation protocol. How it differs from CDP, with raw messages and tested Puppeteer and Selenium code.

By Simon Chiu · Updated

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.

For a tool-level choice, read Puppeteer vs Selenium. To skip the protocol layer, create a free account.

Frequently asked questions

Is WebDriver BiDi faster than the Chrome DevTools Protocol?
Not in a way you will notice for screenshots. In our test on Chrome 155, the median launch and screenshot times with Puppeteer were within about 20 ms of each other on either protocol, and the PNG files were byte-identical.
Does Playwright use WebDriver BiDi?
Not through its public API in version 1.64. Playwright drives Chromium over CDP and its own patched Firefox and WebKit builds over its own protocol. The package contains internal BiDi code, but there is no launch option to switch to it.
Do I need WebDriver BiDi to take screenshots?
No. Classic WebDriver, CDP and BiDi can all capture a page. BiDi matters when you want one protocol for several browsers together with events such as console messages and network requests.

Related in this topic

Try RenderScreenshot free

150 free credits every month, no credit card required.