In Selenium 4's WebDriver BiDi, why does a freshly opened socket deliver no events until `session.subscribe` is sent?
answer
- Opening the channel is not enough
- The remote end waits to be told
- Events or a whole module by name
- Optional context list narrows delivery
- Event frames carry method but no id
basics
~10 sWebDriver BiDi is subscription-based. Connecting to the socket only establishes the channel; the remote end pushes nothing until the client sends session.subscribe naming the events or modules it wants.
solid answer
~40 sIn Selenium 4 the `webSocketUrl` capability gets you a BiDi socket, but the socket starts silent by design — a browser generates far more traffic than any one client wants. The `session.subscribe` command names what to deliver: its `events` list takes full names such as `log.entryAdded` or `network.beforeRequestSent`, or a bare module name such as `network` to take everything that module defines. Its optional `contexts` list narrows delivery to particular browsing contexts; omit it and the subscription is global, covering contexts created later too. From then on the socket carries two kinds of inbound frame: command responses, which carry the `id` of the command they answer, and events, which carry `type: "event"` and a `method` but **no** `id`. `session.unsubscribe` stops delivery again.
code
json · 8 lines{
"id": 3,
"method": "session.subscribe",
"params": {
"events": ["network.beforeRequestSent", "log.entryAdded"],
"contexts": ["0c3f2a1e-7b44-4d3a-9a2e-6f1b0d5c8e21"]
}
}go deeper
Remember that connecting to the BiDi socket is only step one, and that a subscribe command has to name what you want before anything is pushed. Knowing that much keeps you from calling the channel broken.
Explain the mechanics: what the events and contexts arguments accept, that a bare module name takes all its events, that a global subscription covers contexts created later, and how an event frame is told apart from a command response.
Demonstrate ordering judgement — waiting for the subscribe response before the observed action, scoping to a context on a page that opens several, and recognising an empty capture as a subscribe-too-late race rather than a missing feature.
Own the policy: what a suite subscribes to by default, what that costs on every run, who owns the reader-loop code, and how you keep observation from becoming a second uncontrolled source of test flakiness.
## The socket starts silent on purpose When a Selenium 4 session is created with the `webSocketUrl` capability, the remote end opens a **WebDriver BiDi** channel and returns its address. Connecting to it gets you a live channel and nothing else. BiDi is **subscription-based**: the remote end sends no event until the client has named it. The reason is volume. A browser rendering a hotel room-booking calendar produces a request for every month the user pages through, an image per room thumbnail, a console warning each time the date grid re-renders, and a navigation event for every tab. Pushing all of that to every client by default would make the channel unusable and would cost the browser real work to serialise records nobody reads. ## What `session.subscribe` carries The command takes two things that matter in practice: - **`events`** — a list of names. Each entry is either a full event name (`log.entryAdded`, `network.beforeRequestSent`, `browsingContext.load`) or a bare **module** name (`log`, `network`, `browsingContext`), which subscribes to every event that module defines. - **`contexts`** — an optional list of browsing-context ids. Supply it and only those contexts deliver. Omit it and the subscription is **global**, covering every context in the session including ones created after the subscribe call — such as the tab that opens when the calendar's booking confirmation is printed. `session.unsubscribe` is the mirror operation and stops delivery again. Subscribing is cheap to write and not free to run, which is why scoping by context is worth reaching for on a page that opens several. ## Three shapes of frame on one socket Everything on a BiDi socket is JSON, and the shape tells you what you are holding: | Frame | Sent by | Distinguishing members | |---|---|---| | Command | client | `id`, `method`, `params` | | Command response | remote end | `type: "success"`, the matching `id`, `result` | | Error response | remote end | `type: "error"`, the matching `id`, `error`, `message` | | Event | remote end | `type: "event"`, `method`, `params` — and **no `id`** | That last row is the whole trick of reading the channel. A command response can always be paired with the command that caused it because it echoes the `id` the client chose. An **event** was caused by the browser, not by a client command, so there is no id to echo. A client that dispatches purely on `method` and forgets to check for `id` will happily mistake a response for an event. ```json {"type": "event", "method": "network.beforeRequestSent", "params": {"context": "0c3f2a1e-7b44-4d3a-9a2e-6f1b0d5c8e21", "request": {"method": "GET", "url": "https://hotel.example.com/api/availability?month=2026-10"}}} ``` ## What a subscription does not give you Four limits are worth stating before someone is surprised by them: - **Nothing is replayed.** Events for work that happened before the subscribe call are gone. If the calendar fetches October's rates during page load and you subscribe afterwards, that request was never delivered. - **A subscription is not a filter language.** You choose events and contexts, not URLs or log levels; narrowing by request path is the client's job after delivery. - **Events arrive asynchronously**, interleaved with command responses on the same socket, so the client needs a reader loop rather than a call-and-wait shape. - **The channel is per session.** End the session and the socket goes with it; the subscription does not outlive it. ## A worked order of operations 1. Create the session with the `webSocketUrl` capability set to `true`. 2. Read `webSocketUrl` back from the capabilities the new-session response returned; that string is the address to connect to. 3. Connect, then send `session.subscribe` with the events you want — for the booking calendar, `network.beforeRequestSent` and `log.entryAdded`. 4. Wait for that command's success response, keyed by its `id`, before doing anything that should be observed. 5. Drive the calendar over the ordinary WebDriver HTTP endpoints — click the next-month control, wait for the grid — while a reader loop collects the `type: "event"` frames. 6. `session.unsubscribe` when the observation window closes, or simply end the session. Step 4 is the one people skip. Sending subscribe and immediately clicking the month control is a race: the click can be dispatched before the remote end has registered the subscription, and the request you most wanted is the one that goes missing. ## How this reads in an interview The crisp answer is that **BiDi is opt-in per event, per session, and forward-only**: the socket is a delivery mechanism, `session.subscribe` is the switch, and an event is recognisable because it carries a `method` with no `id`.
- The calendar fetches October's rates during page load, but your capture is empty. What happened?The subscription was registered after the fetch. BiDi does not replay history, so any request sent before `session.subscribe` took effect was never delivered. The fix is ordering: create the session, connect the socket, send subscribe and wait for its success response, and only then navigate to the calendar. If the load itself is what you need to observe, subscribe before the navigation command rather than after it.
- How do you keep a client from confusing a command response with a pushed event?Dispatch on the frame's `type` and on the presence of `id`. A response carries `type: "success"` or `type: "error"` together with the `id` the client chose for its command, so it can be matched to a pending call. An event carries `type: "event"` and a `method` with no `id` at all. Matching only on `method` is what makes a client mis-route frames.
saying these in an interview costs you the question
- Believing events flow as soon as the socket is connected
- Expecting events from before the subscribe call to be replayed
- Treating a subscription as a URL or log-level filter
- Thinking each event needs its own separate socket connection
- Assuming an event frame carries an id like a response