In Selenium 4, what does subscribing to every BiDi network event in every test cost a suite?
answer
- Every event you subscribe to costs something
- The consumer is not on your thread
- Scope the subscription, not just the sink
- CopyOnWriteArrayList, never a plain ArrayList
- close() sends session.unsubscribe at teardown
basics
~20 sIn Selenium 4, every subscribed BiDi event crosses the socket and is deserialised by the client, and the callback runs on a connection thread, not the test thread. Narrow the events, keep only failures, and unsubscribe at teardown.
solid answer
~50 sEach subscribed event is serialised by the browser, pushed over the socket and turned into a Java object by the Selenium client, so a gym class-booking board that polls availability produces a steady stream of `beforeRequestSent`, `responseStarted` and `responseCompleted` records per call, multiplied by every test and every parallel worker. The consumer runs on Selenium's BiDi connection thread pool rather than the test thread, so the sink must be thread-safe and an assertion thrown inside the consumer never fails the test. Rather than switching capture off, narrow it: subscribe to `onResponseCompleted` and `onFetchError` only, scope the module to one browsing context with `new Network(browsingContextId, driver)`, filter logs with `FilterBy.logLevel(LogLevel.ERROR)`, and store a small projection instead of the whole record. Close the module at teardown so `session.unsubscribe` is sent and the next test's evidence stays its own.
go deeper
Know that capture is not free and that you subscribe to specific events rather than to everything. Recall that the module should be closed when you are done with it.
Explain what each subscription puts on the socket, which getters are worth retaining, and why the collection has to be thread-safe when the consumer runs off the test thread.
Show how you would size capture for a real suite: which two subscriptions answer which call failed, when to scope to a single browsing context, what projection you retain, and how teardown keeps one test's evidence out of the next.
Own the trade-off between evidence coverage and run cost across the whole suite: which runs carry full capture, what the retention shape is, and how the team avoids paying for records nobody ever reads.
## What a subscription costs on the wire A BiDi subscription is not free-standing bookkeeping inside the browser: every subscribed event is serialised, pushed across the WebSocket, and deserialised by the Selenium client into a Java object. A gym class-booking board that polls availability every few seconds is a good worked case. Each HTTP call it makes can produce, if you subscribed to all of them: - one `network.beforeRequestSent` record when the request is issued, - one `network.responseStarted` record when headers arrive, - one `network.responseCompleted` record when the body is done, - plus a further set per redirect, which `BaseParameters.getRedirectCount()` reports. Multiply that by an availability poll, the class images, the analytics beacons and the booking calls, then by every test in the suite, then by every parallel worker, and "subscribe to everything, filter later" turns into steady socket traffic and a heap that grows for the length of the run. ## Which thread your consumer runs on This is the detail that separates a working harness from a flaky one. `org.openqa.selenium.bidi.Connection` dispatches inbound frames onto a cached pool of daemon threads named **BiDi Connection**. Your `Consumer` therefore runs *off* the test thread, concurrently with the click it is observing. ```java CopyOnWriteArrayList<String> failures = new CopyOnWriteArrayList<>(); network.onResponseCompleted( response -> { if (response.getResponseData().getStatus() >= 400) { failures.add( response.getRequest().getMethod() + " " + response.getRequest().getUrl()); } }); ``` Three rules follow, and each is a real defect when broken: 1. **Use a thread-safe sink.** A plain `ArrayList` written from the pool while the test thread reads it is a data race; `CopyOnWriteArrayList` or `ConcurrentLinkedQueue` costs nothing at these volumes. 2. **Never assert inside the consumer.** An exception raised there is thrown on a pooled thread; the test thread never sees it, so a broken expectation reads as a pass. 3. **Keep the consumer cheap.** It runs for every event; a per-event write to disk or a network call of your own turns capture into the slowest part of the run. ## Narrowing the subscription instead of dropping it The answer to cost is rarely "turn capture off", because then the failing run has no evidence. It is to subscribe to less: | Lever | How | What you give up | |---|---|---| | Fewer events | Subscribe to `onResponseCompleted` and `onFetchError` only | Request-issue timing and header-arrival timing | | One browsing context | `new Network(browsingContextId, driver)` | Traffic from other tabs and windows | | Log severity | `onConsoleEntry(consumer, FilterBy.logLevel(LogLevel.ERROR))` | Warnings and informational console output | | Shorter window | Open the module around the risky step, not the whole test | Context from earlier in the test | `onResponseCompleted` plus `onFetchError` is the pairing that answers "which call failed", because a request that never got an answer at all — DNS failure, connection reset, an aborted fetch — produces a `FetchError` record with `getErrorText()` and no response event whatsoever. Subscribing only to response events makes those failures invisible, which is the exact case a booking board hits when its API host is unreachable. ## Bounding what you keep Retention is a separate decision from subscription, and it is where memory actually goes: - Store a projection, not the record. `getMethod()`, `getUrl()`, `getStatus()`, `getTimestamp()` and `getRequestId()` as a small immutable value is a fraction of the object graph behind `ResponseDetails`. - Keep failures unconditionally and successes only if you need a timeline; a bounded deque of the last N entries is usually enough context. - Do not plan on the payload: `ResponseData.getContent()` returns an `Optional<Long>` size, not the body, so retaining whole records buys you no more of the response than a projection does. ## Unsubscribing at teardown `Network` and `LogInspector` are `AutoCloseable`, and closing sends `session.unsubscribe` for the events that object subscribed. On a suite that reuses a driver across tests, forgetting this has two effects at once: - The socket keeps carrying events nobody reads, for the remainder of the run. - The previous test's listener keeps appending to a sink the next test will report as its own, so the booking test inherits requests from the cancellation test that ran before it. Close the module in the same scope that opened it — a try-with-resources block that also contains the interaction under test — and the subscription window matches the evidence window exactly. When a helper hands the module back to a caller, hand back the `AutoCloseable` too, so the teardown that owns the driver also owns the unsubscribe. The one thing not to do is leave a permanently-subscribed module on a shared driver and rely on filtering at report time: that pays the wire and heap cost of every event for the whole run in exchange for evidence you could have scoped for free.
- Why is onFetchError worth subscribing to alongside onResponseCompleted?A request that never gets an answer — DNS failure, a reset connection, an aborted fetch — produces no response event at all. `onFetchError` delivers a `FetchError` record whose `getErrorText()` names the reason, so a booking board whose API host is unreachable still leaves evidence instead of an empty capture that reads like a clean network.
- What goes wrong if the test asserts inside the event consumer?The consumer runs on a Selenium BiDi connection thread, so an assertion error is raised there and never propagates to the test thread. The test carries on and passes. Collect into a thread-safe sink and assert on that sink from the test thread once the step under observation has finished.
- How do you keep capture from growing without bound on a long test?Store a projection rather than the record: method, URL, status, timestamp and request id are enough to identify a call. Keep failures unconditionally and successes in a bounded deque if you want a timeline. Retaining whole `ResponseDetails` objects buys nothing extra, since the body is not in the event.
saying these in an interview costs you the question
- Keeps every request and response object for the whole run in memory
- Asserts inside the event consumer and expects the test to fail
- Adds listeners per test on a reused driver and never unsubscribes
- Subscribes to response events only and misses never-answered requests
- Expects the response body to arrive inside the event payload