Skip to main content
SeleniumDecoded

How WebDriver Works: Architecture and Protocol

What actually happens when you call driver.findElement(): the client binding, the W3C WebDriver protocol, the browser driver, sessions, and where BiDi and Grid fit in.

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

“Explain the Selenium architecture” is a guaranteed interview question, and understanding it is also the fastest way to debug problems like timeouts, stale elements and version mismatches. This lesson walks through one command end to end.

The Four Pieces

┌──────────────┐ HTTP (W3C WebDriver) ┌──────────────┐ native / IPC ┌───────────┐
│ Your test │ ───────────────────────▶ │ Browser │ ───────────────▶ │ Browser │
│ (Java, │ POST /session/{id}/ │ driver │ │ (Chrome, │
│ Python, ...)│ element │ (chromedriver│ │ Firefox, │
│ + Selenium │ ◀─────────────────────── │ geckodriver │ ◀─────────────── │ Safari) │
│ binding │ {"value": {...}} │ safaridriver│ │ │
└──────────────┘ └──────────────┘ └───────────┘
│ ▲
└── WebSocket (WebDriver BiDi) ───────────┘ events flow back: console, network, DOM
  1. The language binding (selenium-java, selenium for Python, selenium-webdriver for Node, Selenium.WebDriver for .NET) turns method calls into HTTP requests.
  2. The W3C WebDriver protocol defines those requests: endpoints, JSON bodies, error codes. It is a browser-neutral standard; Selenium 4 speaks only this (the older JSON Wire Protocol is gone).
  3. The browser driver is an executable shipped by the browser vendor (chromedriver by Google, geckodriver by Mozilla, msedgedriver by Microsoft, safaridriver by Apple). It is an HTTP server that translates protocol commands into the browser’s internal automation interface.
  4. The browser does the work.

Selenium Manager sits before step 3, making sure the driver executable exists and matches the browser. Selenium Grid sits between steps 1 and 3, routing commands from many tests to many drivers on many machines.

One Command, End to End

Take this line:

A single findElement call
Selenium 4 Stable
WebElement button = driver.findElement(By.cssSelector("button.submit"));
button.click();
button = driver.find_element(By.CSS_SELECTOR, "button.submit")
button.click()
const button = await driver.findElement(By.css('button.submit'));
await button.click();
IWebElement button = driver.FindElement(By.CssSelector("button.submit"));
button.Click();

The binding sends:

POST /session/8f2a.../element HTTP/1.1
Content-Type: application/json
{"using": "css selector", "value": "button.submit"}

The driver asks the browser to run the query, and replies:

{"value": {"element-6066-11e4-a52e-4f735466cecf": "f.1A2B3C.d.4D5E.e.7"}}

That long key is the W3C element identifier; the value is an opaque reference to the DOM node. Your WebElement object is a wrapper around that reference plus the session. click() then sends:

POST /session/8f2a.../element/f.1A2B3C.d.4D5E.e.7/click

Three consequences fall straight out of this:

  • Every command is a network round trip. Chaining 200 findElement calls costs 200 requests. This is why Selenium is slower per command than tools with a persistent socket, and why batching through JavaScript or BiDi helps.
  • A WebElement is a reference, not a snapshot. If the page replaces that DOM node, the reference is dead and the driver returns stale element reference. Re-find it.
  • The driver, not Selenium, decides what “clickable” means. Element interactability checks (visible, not obscured, in viewport) are implemented in the driver per the spec. Differences between browsers usually live here.

Sessions

A session is one browser instance under control. Creating the driver sends POST /session with your capabilities; the driver launches the browser and returns a session id and the negotiated capabilities. driver.quit() sends DELETE /session/{id}, closing the browser and the driver process. driver.close() only closes the current window; if it was the last window the session ends too, but the driver process may linger. Always quit() in a finally or a fixture.

Inspecting the negotiated session
Selenium 4 Stable
RemoteWebDriver driver = new ChromeDriver();
System.out.println(driver.getSessionId());
Capabilities caps = driver.getCapabilities();
System.out.println(caps.getBrowserName() + " " + caps.getBrowserVersion());
System.out.println(caps.getCapability("webSocketUrl")); // present when BiDi is enabled
driver = webdriver.Chrome()
print(driver.session_id)
print(driver.capabilities["browserName"], driver.capabilities["browserVersion"])
print(driver.capabilities.get("webSocketUrl")) # present when BiDi is enabled
const driver = await new Builder().forBrowser('chrome').build();
const session = await driver.getSession();
console.log(session.getId());
const caps = await driver.getCapabilities();
console.log(caps.getBrowserName(), caps.getBrowserVersion());
var driver = new ChromeDriver();
Console.WriteLine(driver.SessionId);
Console.WriteLine($"{driver.Capabilities.GetCapability("browserName")} {driver.Capabilities.GetCapability("browserVersion")}");

The W3C Standard and Why It Matters

Selenium 1 injected JavaScript into pages. Selenium 2 introduced WebDriver and the JSON Wire Protocol, a de facto standard Selenium wrote. Selenium 4 completed the move to the W3C WebDriver specification, which browser vendors implement themselves. Practical effects:

  • Vendors own compatibility. When Chrome ships, chromedriver ships with it. Selenium does not need a release for a new browser version.
  • Capabilities are strict. Unknown top-level keys are rejected; vendor extensions live under prefixes like goog:chromeOptions, moz:firefoxOptions, ms:edgeOptions, se:options (Grid) or bstack:options (cloud).
  • Actions are precise. The W3C Actions API models input devices (pointer, key, wheel, pen) with explicit ticks, which is what Actions / ActionChains build.
  • Appium speaks the same protocol, so mobile automation reuses the same client code.

Where BiDi Fits

Classic WebDriver is strictly request-response over HTTP; the browser can never call you. WebDriver BiDi adds a WebSocket alongside the session (advertised in the webSocketUrl capability). Over it the browser pushes events: console messages, JavaScript errors, network requests, DOM mutations, navigation. It also carries commands that classic WebDriver lacks, such as intercepting a request or overriding geolocation. BiDi is a W3C specification too, so it works across Chrome, Edge, Firefox and (as of 4.47, in Technology Preview) Safari. See WebDriver BiDi.

CDP, the Chrome DevTools Protocol, is the Chromium-only predecessor Selenium 4 exposed before BiDi matured. It is version-specific and on its way out; see Chrome DevTools Protocol.

Where Grid Fits

With a Grid, your binding points RemoteWebDriver at the Grid’s Router URL instead of a local driver. The Router forwards POST /session to a queue, the Distributor picks a Node with a matching stereotype, the Node starts a driver and browser, and every later command is proxied to that Node. From your test’s perspective nothing changes but the URL; from the infrastructure’s perspective you get parallelism, cross-platform coverage and, on Kubernetes, browsers on demand. See Selenium Grid.

Debugging With This Model

SymptomWhich layerFirst thing to check
session not created: This version of ChromeDriver only supports...Driver vs browserDriver mismatch; remove hard-coded driver paths so Selenium Manager resolves it
Test hangs for exactly your page load timeoutDriver waiting for loadUse pageLoadStrategy eager or wait for a specific element
stale element referenceElement referenceThe DOM node was replaced; re-locate after the change
element click interceptedDriver interactability checkSomething overlays the element; wait for it to disappear or scroll
Works in Chrome, fails in FirefoxDriver implementationCheck the spec behaviour (for example sendKeys to a file input, or focus rules)
Commands slow on Grid but fast locallyNetwork between binding and NodeMeasure round-trip time; move the test runner closer to the Grid

Turn on driver logs to see the protocol traffic itself: chromedriver with --verbose (via ChromeDriverService), geckodriver with --log trace. The Browser and Driver Logs lesson shows each binding.

Summary

  • Binding builds HTTP requests, the W3C protocol defines them, the vendor’s driver executes them in the browser.
  • Every command is a round trip; every WebElement is a live reference that can go stale.
  • Selenium 4 is W3C-only, which is why capabilities need vendor prefixes and why vendors keep drivers compatible.
  • BiDi adds a WebSocket for events and interception; Grid adds routing to remote drivers. Neither changes how your test code reads.

Related lessons