skip to content

In Selenium, how does each parallel worker's driver process avoid binding the same port?

level: middleimportance: should knowfreq 35%

answer

  1. The driver is an HTTP server
  2. Zero means choose for me
  3. Something probes before the process starts
  4. The builder forgets the port after building
  5. usingPort is what breaks it

basics

~20 s

Selenium's driver service builder defaults its port to zero, and build() replaces zero with a port proved free by binding a socket. Each worker that constructs its own driver therefore gets its own port, unless someone pins one with usingPort.

solid answer

~40 s

Every driver is a local HTTP server, so two of them on one port is a bind failure. Selenium avoids it by default: `new ChromeDriver()` goes through `ChromeDriverService.createDefaultService()`, the builder's port starts at `0`, and `DriverService.Builder.build()` turns that `0` into a call to `PortProber.findFreePort()`, which picks a port outside the operating system's ephemeral range and proves it free by binding a socket. `build()` then resets the field to `0`, so even a reused builder keeps allocating fresh ports. Collisions come from taking that choice away - `usingPort(9515)` on every worker is the usual culprit, and `usingAnyFreePort()` undoes it. Sharing one `ChromeDriverService` across workers is a different thing and is legitimate: one driver process hosts many sessions on one port with no conflict.

code

java · 16 lines
java
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeDriverService;
import org.openqa.selenium.chrome.ChromeOptions;

public class CatalogueDriverFactory {

  public static ChromeDriver forWorker(String workerId) {
    ChromeDriverService service =
        new ChromeDriverService.Builder().usingAnyFreePort().build();

    ChromeOptions options = new ChromeOptions();
    options.addArguments("--user-data-dir=/tmp/museum-catalogue-" + workerId);

    return new ChromeDriver(service, options);
  }
}

go deeper

for a junior

Know that each driver is a small local server on a port, and that constructing a driver the ordinary way already picks a free one. You are not expected to configure ports by hand.

for a middle

Explain the mechanism end to end: port zero as a sentinel, the free-port probe before launch, the builder resetting itself, and which single call turns a safe default into a collision.

for a senior

Show how you recognise a port collision from its shape - failing at driver start, all workers but one, scaling exactly with worker count - and separate it from causes that only appear on a CI agent.

for a principal

Decide whether workers get a driver process each or share one service, and be explicit about the tradeoff between process count on the host and blast radius when a shared driver dies.

## The port a driver process listens on Every browser driver - `chromedriver`, `geckodriver`, `msedgedriver` - is a **local HTTP server**, and Selenium's client speaks the W3C WebDriver protocol to it over a TCP port. Two driver processes bound to the same port is a plain `bind()` failure, and in a parallel run that means one worker alive and the rest dead before a single page loads. ## Selenium's default already avoids the collision - `new ChromeDriver()` calls `ChromeDriverService.createDefaultService()`, which builds a service through `ChromeDriverService.Builder`. - The builder's port field starts at **0**, and `DriverService.Builder.build()` reads 0 as "choose one for me": it calls `PortProber.findFreePort()` before the driver process is launched. - `PortProber.findFreePort()` picks a random port **outside the operating system's ephemeral range** - deliberately, so the choice does not race the OS's own allocations - skips a list of ports browsers refuse to connect to, and proves the candidate free by binding a `ServerSocket` on both `localhost` and `0.0.0.0`. It retries up to ten times, then throws `Unable to find a free port`. - `build()` afterwards resets the builder's port back to 0, so **reusing one builder across workers keeps allocating fresh ports** instead of repeating the first one. So the out-of-the-box shape, where each worker of a museum exhibit catalogue suite constructs its own driver, is port-safe with no configuration at all. ```java new ChromeDriverService.Builder().build(); // free port, safe in parallel new ChromeDriverService.Builder().usingPort(9515).build(); // pinned, collides at N > 1 ``` ## What actually makes it collide 1. **Pinning the driver port.** `usingPort(9515)` on every worker is the classic cause: the number is copied from a debugging session, works serially, and breaks the moment two workers build a service at once. `usingAnyFreePort()` puts a builder back to 0. 2. **Pinning a browser-side port** through the browser's own launch arguments does the same at a different layer, because the browser binary binds it, not the driver. 3. **Pinning geckodriver's BiDi websocket port.** `GeckoDriverService.Builder.withWebSocketPort(n)` fixes that port; leaving it unset makes Selenium pass `--websocket-port 0`, which auto-allocates. ## Sharing one service is not the same as colliding | Setup | Driver processes | Port outcome | |---|---|---| | Each worker builds its own service, port unset | one per worker | a distinct free port each | | Each worker builds its own service with `usingPort(9515)` | one per worker | all fight over 9515 | | Every worker shares one `ChromeDriverService` | one, many sessions | one port, no conflict | | Workers use `RemoteWebDriver` against a grid URL | none locally | one remote endpoint | A **single shared driver service** is a legitimate configuration rather than a bug: one `chromedriver` process can host many sessions, so N workers on one service means one port and no collision. You trade isolation of the driver process for a much lower process count. What is never legitimate is N services each told to use the same fixed number. ## Recognising a port collision from the outside - The failure lands at **driver start**, before any navigation, and the first worker in usually survives. That asymmetry is the tell. - The driver's own log ends with an address-already-in-use bind error, while the Selenium side reports only that the session was never created. - The failure count tracks the worker count exactly: two workers, one failure; six workers, five. - It reproduces on a laptop with the same configuration, which separates it from causes that only appear on a CI agent. To confirm it in one pass: 1. Run the suite with a single worker - a pinned port cannot collide with itself, so it goes green. 2. Grep the configuration for a literal port number reaching a `DriverService.Builder`. 3. Delete the pin, or replace it with `usingAnyFreePort()`, and re-run at the original worker count. ## Where the port sits among the other per-worker resources Getting the port right does not make a worker independent. It still needs its own driver session, its own profile directory and its own download directory, and the host still imposes a memory ceiling on how many browsers can run at once. The port is the cheapest of those to get right, because Selenium gets it right for you unless you deliberately take the choice away.

  • Is sharing a single ChromeDriverService across every parallel worker a bug?
    No. One chromedriver process can host many sessions, so N workers on one service means one port and no conflict, and far fewer processes on the host. What you give up is isolation: a driver process that dies takes every session with it. The bug is N separate services all pinned to the same fixed port.
  • Why does PortProber deliberately avoid the operating system's ephemeral port range?
    Because the kernel hands out ephemeral ports to outbound connections without asking anyone. A port that probes free inside that range can be taken between the probe and the driver's bind, and Selenium opens enough sockets for that race to happen often. Choosing outside the range makes the probe's answer far more likely to still hold.

saying these in an interview costs you the question

  • Says every chromedriver always listens on 9515 regardless of how it was started
  • Believes Selenium cannot run two drivers on one machine at all
  • Thinks sharing one driver service between workers is itself a port collision
  • Assumes a reused builder repeats the port it allocated the first time
  • Fixes a bind failure with a retry loop instead of removing the pinned port