Chrome DevTools Protocol (CDP)
What CDP gives Selenium on Chromium browsers, how to use it for network throttling, device emulation and performance metrics, why it is version-locked, and when to choose BiDi instead.
The Chrome DevTools Protocol is what Chrome’s own DevTools panel uses to talk to the browser. Selenium 4 exposed it so tests could do things WebDriver could not: throttle the network, emulate a phone, read performance metrics, intercept requests. Then WebDriver BiDi arrived to do most of that in a standard, cross-browser way. In 2026 CDP is still available on Chromium browsers, still useful for a few Chromium-only features, and firmly on the path out: Firefox support was removed in Selenium 4.29 and the Selenium 5 roadmap removes CDP-only code paths. This lesson tells you what it can still do and how to use it without painting yourself into a corner.
The Version Problem
CDP is not a standard. Its commands change with every Chrome release, and Chrome now releases every two weeks. Selenium ships per-version DevTools packages (selenium-devtools-v144, v143, v142 in Java; the equivalent modules in Python and .NET) and keeps only the latest three. Since 4.45 there is also a selenium-devtools-latest artifact. If your Chrome is newer than your DevTools package, you get a warning and Selenium falls back to the closest version, which usually works and occasionally does not.
BiDi has no such problem, which is the main reason to prefer it.
Getting a DevTools Session
import org.openqa.selenium.chrome.ChromeDriver;import org.openqa.selenium.devtools.DevTools;import org.openqa.selenium.devtools.v144.network.Network; // pin to your Chrome's major version
ChromeDriver driver = new ChromeDriver();DevTools devTools = driver.getDevTools();devTools.createSession();
// Enable a domain before using itdevTools.send(Network.enable(Optional.empty(), Optional.empty(), Optional.empty()));driver = webdriver.Chrome()
# Python sends commands by name; no per-version import neededdriver.execute_cdp_cmd("Network.enable", {})
# For typed, versioned access:# from selenium.webdriver.common.devtools.v144 import network# async with driver.bidi_connection() as session:# await session.session.execute(network.enable())const driver = await new Builder().forBrowser('chrome').build();
// Raw CDP over the driver's connectionawait driver.sendDevToolsCommand('Network.enable', {});
// Or a full connection for eventsconst cdp = await driver.createCDPConnection('page');using OpenQA.Selenium.DevTools;using DevToolsSessionDomains = OpenQA.Selenium.DevTools.V144.DevToolsSessionDomains;
var driver = new ChromeDriver();IDevToolsSession session = driver.GetDevToolsSession();var domains = session.GetVersionSpecificDomains<DevToolsSessionDomains>();
await domains.Network.Enable(new OpenQA.Selenium.DevTools.V144.Network.EnableCommandSettings());Python’s execute_cdp_cmd is the least fragile entry point: it sends a raw command by name and lets Chrome validate it, so you never import a versioned module.
Network Throttling
Simulate a slow 3G connection to test loading states. BiDi gained setNetworkConditions in 4.40 but CDP’s version has been stable for years and is a fair use of it today.
import org.openqa.selenium.devtools.v144.network.model.ConnectionType;
devTools.send(Network.emulateNetworkConditions( false, // offline 150, // latency ms 400 * 1024 / 8, // download throughput bytes/s (400 kbps) 200 * 1024 / 8, // upload throughput bytes/s Optional.of(ConnectionType.CELLULAR3G), Optional.empty(), Optional.empty(), Optional.empty()));
driver.get("https://app.example.com/catalog");assertTrue(driver.findElement(By.cssSelector(".skeleton-loader")).isDisplayed());driver.execute_cdp_cmd("Network.enable", {})driver.execute_cdp_cmd("Network.emulateNetworkConditions", { "offline": False, "latency": 150, "downloadThroughput": 400 * 1024 // 8, "uploadThroughput": 200 * 1024 // 8, "connectionType": "cellular3g",})
driver.get("https://app.example.com/catalog")assert driver.find_element(By.CSS_SELECTOR, ".skeleton-loader").is_displayed()
# Go offline entirelydriver.execute_cdp_cmd("Network.emulateNetworkConditions", {"offline": True, "latency": 0, "downloadThroughput": 0, "uploadThroughput": 0})await driver.sendDevToolsCommand('Network.enable', {});await driver.sendDevToolsCommand('Network.emulateNetworkConditions', {offline: false,latency: 150,downloadThroughput: (400 * 1024) / 8,uploadThroughput: (200 * 1024) / 8,connectionType: 'cellular3g',});
await driver.get('https://app.example.com/catalog');await domains.Network.EmulateNetworkConditions(new OpenQA.Selenium.DevTools.V144.Network.EmulateNetworkConditionsCommandSettings{ Offline = false, Latency = 150, DownloadThroughput = 400 * 1024 / 8, UploadThroughput = 200 * 1024 / 8, ConnectionType = OpenQA.Selenium.DevTools.V144.Network.ConnectionType.Cellular3g,});
driver.Navigate().GoToUrl("https://app.example.com/catalog");Device Emulation and Geolocation
Chrome’s mobileEmulation option (no CDP needed) handles the common case of “pretend to be a phone.” CDP adds fine control and geolocation.
// Options-based device emulation: stable, no CDPChromeOptions options = new ChromeOptions();options.setExperimentalOption("mobileEmulation", Map.of("deviceName", "Pixel 7"));ChromeDriver driver = new ChromeDriver(options);
// Geolocation via CDPDevTools devTools = driver.getDevTools();devTools.createSession();devTools.send(Emulation.setGeolocationOverride( Optional.of(51.5074), Optional.of(-0.1278), Optional.of(1.0), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty(), Optional.empty()));options = webdriver.ChromeOptions()options.add_experimental_option("mobileEmulation", {"deviceName": "Pixel 7"})driver = webdriver.Chrome(options=options)
driver.execute_cdp_cmd("Emulation.setGeolocationOverride", { "latitude": 51.5074, "longitude": -0.1278, "accuracy": 1,})
# Custom device instead of a named oneoptions.add_experimental_option("mobileEmulation", { "deviceMetrics": {"width": 390, "height": 844, "pixelRatio": 3.0}, "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) ...",})const options = new chrome.Options().setMobileEmulation({ deviceName: 'Pixel 7' });const driver = await new Builder().forBrowser('chrome').setChromeOptions(options).build();
await driver.sendDevToolsCommand('Emulation.setGeolocationOverride', {latitude: 51.5074, longitude: -0.1278, accuracy: 1,});var options = new ChromeOptions();options.EnableMobileEmulation("Pixel 7");var driver = new ChromeDriver(options);
var session = driver.GetDevToolsSession();await session.SendCommand("Emulation.setGeolocationOverride", JsonSerializer.SerializeToNode(new{ latitude = 51.5074, longitude = -0.1278, accuracy = 1,}));For geolocation on Firefox, use BiDi’s emulation.setGeolocationOverride; see WebDriver BiDi.
Performance Metrics
CDP’s Performance domain is genuinely Chromium-only and has no BiDi equivalent yet. It is a legitimate reason to keep CDP in the toolbox.
import org.openqa.selenium.devtools.v144.performance.Performance;import org.openqa.selenium.devtools.v144.performance.model.Metric;
devTools.send(Performance.enable(Optional.empty()));driver.get("https://app.example.com/dashboard");
List<Metric> metrics = devTools.send(Performance.getMetrics());metrics.stream() .filter(m -> List.of("JSHeapUsedSize", "Nodes", "LayoutCount", "ScriptDuration").contains(m.getName())) .forEach(m -> System.out.println(m.getName() + " = " + m.getValue()));driver.execute_cdp_cmd("Performance.enable", {})driver.get("https://app.example.com/dashboard")
metrics = {m["name"]: m["value"] for m in driver.execute_cdp_cmd("Performance.getMetrics", {})["metrics"]}for name in ("JSHeapUsedSize", "Nodes", "LayoutCount", "ScriptDuration"): print(name, "=", metrics[name])
# Also useful: Navigation Timing via plain JavaScript, which works in every browsertiming = driver.execute_script("return performance.getEntriesByType('navigation')[0].toJSON()")print("DOM interactive:", timing["domInteractive"], "ms")await driver.sendDevToolsCommand('Performance.enable', {});await driver.get('https://app.example.com/dashboard');
const { metrics } = await driver.sendDevToolsCommand('Performance.getMetrics', {});for (const m of metrics) {if (['JSHeapUsedSize', 'Nodes', 'LayoutCount', 'ScriptDuration'].includes(m.name)) console.log(m.name, m.value);}await domains.Performance.Enable(new OpenQA.Selenium.DevTools.V144.Performance.EnableCommandSettings());driver.Navigate().GoToUrl("https://app.example.com/dashboard");
var result = await domains.Performance.GetMetrics(new OpenQA.Selenium.DevTools.V144.Performance.GetMetricsCommandSettings());foreach (var m in result.Metrics.Where(m => new[] { "JSHeapUsedSize", "Nodes", "LayoutCount", "ScriptDuration" }.Contains(m.Name))) Console.WriteLine($"{m.Name} = {m.Value}");CDP or BiDi? A Decision Table
| Need | Use | Why |
|---|---|---|
| Console logs, JS errors | BiDi | Cross-browser, high-level API |
| Basic auth | BiDi | Cross-browser, one line |
| Block, modify, mock requests | BiDi | Cross-browser; CDP Fetch domain is the legacy path |
| Geolocation | BiDi (Firefox required) or CDP (Chromium only) | BiDi API stabilising through 4.4x |
| Network throttling | Either | BiDi setNetworkConditions since 4.40; CDP older and well-tested |
| Mobile viewport | Chrome mobileEmulation option | No protocol needed |
| Performance metrics, coverage | CDP | No BiDi equivalent |
| Full-page screenshot beyond viewport | CDP Page.captureScreenshot | BiDi captureScreenshot supports a clip since 4.2x; check your version |
| Anything on Firefox or Safari | BiDi | CDP unavailable |
Writing CDP Code That Survives Upgrades
- Use raw command names (
execute_cdp_cmd,sendDevToolsCommand,SendCommand) instead of versioned typed modules where your language allows. Chrome validates the command; your code has no version in it. - Isolate CDP calls in one helper class with a BiDi implementation next to it, so switching is a one-line change.
- Pin Chrome in CI with
browserVersionso the DevTools package and browser move together on your schedule, not Google’s. - Never use CDP for something BiDi already does; the CDP path is the one that will be deleted.
Summary
- CDP is Chromium’s own protocol: powerful, version-locked, Chromium-only, and being phased out of Selenium in favour of BiDi.
- Still the right tool for performance metrics and a few emulation details; wrong for logs, auth and interception now that BiDi covers them.
- Send raw commands and isolate the code so the eventual migration is trivial.