How Does Chrome DevTools Protocol Work?
Learn how CDP works: the WebSocket transport, domains and commands, targets and sessions, and how Playwright and Puppeteer build on it.
The Chrome DevTools Protocol is how Chrome exposes itself to be inspected and controlled. DevTools itself is just a client of it β which is why anything you can do in DevTools, a script can do too.
1. The Transport
Start Chrome with a debugging port:
chrome --remote-debugging-port=9222
An HTTP endpoint then lists what is available:
GET http://localhost:9222/json/version
GET http://localhost:9222/json/list
These return a webSocketDebuggerUrl. Connect to it, and all further communication is JSON messages over that WebSocket.
The HTTP part exists only for discovery. Everything real happens on the socket.
2. Commands and Events
Messages follow JSON-RPC conventions.
A command carries an id, a method and parameters:
{"id": 1, "method": "Page.navigate", "params": {"url": "https://example.com"}}
Its response matches the id:
{"id": 1, "result": {"frameId": "ABC123"}}
Events are unsolicited and have no id:
{"method": "Network.requestWillBeSent", "params": {"request": {...}}}
Events must be enabled per domain before they arrive:
{"id": 2, "method": "Network.enable"}
Forgetting to enable a domain is the most common first-time CDP mistake β commands work, and the events you are waiting for never come.
3. Domains
The protocol is organised into namespaces:
| Domain | Covers |
|---|---|
| Page | Navigation, lifecycle, screenshots, PDF |
| Network | Requests, responses, interception, cookies |
| Runtime | JavaScript evaluation, console |
| DOM | Document tree, node queries |
| Input | Synthetic mouse, keyboard, touch |
| Emulation | Device metrics, geolocation, timezone |
| Target | Tabs, frames, workers |
| Fetch | Request interception and modification |
| Tracing | Performance traces |
There are dozens more, some stable and some explicitly experimental.
4. Targets and Sessions
A browser is not one thing to control. Each tab, iframe, service worker and extension is a target.
{"id": 3, "method": "Target.attachToTarget",
"params": {"targetId": "TAB1", "flatten": true}}
The response carries a sessionId. Subsequent messages for that target must include it:
{"id": 4, "sessionId": "S1", "method": "Page.navigate", "params": {...}}
With flatten: true, all sessions share one WebSocket and are distinguished by sessionId. Without it, sessions nest inside wrapper messages, which is harder to work with.
This matters for anyone writing a client from scratch: one CDP WebSocket URL is one browser, and reaching multiple tabs means attaching to each target and stamping every message with the right session.
5. What Libraries Add
Raw CDP is verbose and stateful. Playwright and Puppeteer wrap it with:
- Method ergonomics.
page.click(selector)expands into resolving the node, computing its box, checking visibility, and dispatching input events at coordinates. - Waiting. Correlating events to know when a navigation or a request has genuinely finished.
- Session management. Attaching to targets and routing messages without you tracking ids.
- Cross-browser abstraction. Playwright maps the same API onto Firefox and WebKit protocols.
Both keep an escape hatch:
const client = await page.createCDPSession();
await client.send('Network.emulateNetworkConditions', {...});
Use it when the wrapper does not expose something. It is a normal thing to do, not a hack.
6. Practical Notes
- Remote CDP is not a proxy target. Connecting to a remote browser gives you that browser; per-tab URLs and connection-level tricks that work locally often do not translate.
- Some domains are experimental and change between Chrome versions without deprecation.
- Emulation is not always what it seems.
Emulation.setDeviceMetricsOverridechanges the rendering viewport, which is not the same as the real window size β screenshot dimensions and DOM measurements can disagree. - Enable before you listen, or events will silently never fire.
- The connection is the session. Losing the WebSocket loses everything attached to it.
7. Real-World Examples
- A rendering service driving
Page.printToPDFdirectly, without a wrapper library. - Request blocking via
Fetch.enablewith URL patterns, cutting page weight substantially. - A custom client stamping
sessionIdon every message afterattachToTargetwithflatten. - A debugging session dropping to
createCDPSessionfor a protocol call the library did not wrap. - A viewport bug where a requested width never applied over remote CDP, while an explicit screenshot clip did.
8. Summary
CDP exposes Chrome over a WebSocket carrying JSON-RPC messages: commands with ids, matching responses, and events that must be enabled per domain first. Functionality is grouped into domains covering pages, network, runtime, DOM, input, emulation and targets.
Everything controllable is a target β tabs, frames, workers β reached by attaching and then stamping each message with the returned sessionId, with flatten: true keeping it all on one connection.
Playwright and Puppeteer wrap this with ergonomics, waiting logic and session management, while leaving direct protocol access available for anything they do not cover. That escape hatch is where most non-trivial browser automation eventually goes.