Skip to main content
SeleniumDecoded

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.

Selenium 4 Fragile Updated 9 Sept 2026 · Verified against Selenium 4.48.0

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

Open a CDP session (Chromium only)
Selenium 4 Fragile
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 it
devTools.send(Network.enable(Optional.empty(), Optional.empty(), Optional.empty()));
driver = webdriver.Chrome()
# Python sends commands by name; no per-version import needed
driver.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 connection
await driver.sendDevToolsCommand('Network.enable', {});
// Or a full connection for events
const 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.

Emulate a slow connection
Selenium 4 Fragile
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 entirely
driver.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.

Mobile emulation via options, geolocation via CDP
Selenium 4 Medium
// Options-based device emulation: stable, no CDP
ChromeOptions options = new ChromeOptions();
options.setExperimentalOption("mobileEmulation", Map.of("deviceName", "Pixel 7"));
ChromeDriver driver = new ChromeDriver(options);
// Geolocation via CDP
DevTools 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 one
options.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.

Read page performance counters
Selenium 4 Fragile
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 browser
timing = 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

NeedUseWhy
Console logs, JS errorsBiDiCross-browser, high-level API
Basic authBiDiCross-browser, one line
Block, modify, mock requestsBiDiCross-browser; CDP Fetch domain is the legacy path
GeolocationBiDi (Firefox required) or CDP (Chromium only)BiDi API stabilising through 4.4x
Network throttlingEitherBiDi setNetworkConditions since 4.40; CDP older and well-tested
Mobile viewportChrome mobileEmulation optionNo protocol needed
Performance metrics, coverageCDPNo BiDi equivalent
Full-page screenshot beyond viewportCDP Page.captureScreenshotBiDi captureScreenshot supports a clip since 4.2x; check your version
Anything on Firefox or SafariBiDiCDP 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 browserVersion so 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.

Related lessons