PRACTICAL GUIDE / Selenium BiDi browser cache testing
Test Browser Cache Behavior with Selenium BiDi
Learn Selenium BiDi browser cache testing with working code, failure cases, debugging steps, and CI evidence for reliable QA automation in practice.
In this guide11 sections
- Define the Cache Claim Before Measuring It
- Map the Cache Layers and Choose Controls
- Verify setCacheBehavior and Event Support
- Build a Fixture with Origin-Backed Evidence
- Execute the Cold, Warm, and Bypass Workflow
- Correlate Events Without Introducing an Intercept
- Add Negative Controls for Cache Keys and Isolation
- Diagnose False Hits, Misses, and Missing Events
- Restore State and Operate the CI Gate
- Frequently Asked Questions
- What does ResponseData.isFromCache prove?
- Does CacheBehavior.BYPASS clear the browser cache?
- How do I create a reliable warm-cache test fixture?
- Why can a service worker invalidate a cache assertion?
- What cleanup does a cache-behavior test own?
- How should cache tests run in parallel CI?
- Practice a Cache Experiment in QABattle
What you will learn
- Define the Cache Claim Before Measuring It
- Map the Cache Layers and Choose Controls
- Verify setCacheBehavior and Event Support
- Build a Fixture with Origin-Backed Evidence
Selenium BiDi browser cache testing needs three aligned signals: a unique cacheable resource, responseCompleted events correlated by request ID, and origin-backed content showing whether the server ran. Fetch cold and warm under DEFAULT, repeat under BYPASS, restore the prior mode in finally, and isolate service workers, CDNs, and parallel browser state from the claim.
Define the Cache Claim Before Measuring It
"The page used cache" is too vague for an automated verdict. Name the cache layer, resource, context, and consequence. A useful browser HTTP cache contract might say: after one successful response with Cache-Control: public, max-age=600, a second fetch of the identical URL in the same browsing context should use a local response, avoid another origin execution, and render the same body. A bypass fetch should execute the origin again.
This claim requires browser evidence and server evidence. Selenium's ResponseData.isFromCache() tells you whether the response cache state reported through BiDi is local. A test-only origin hit number embedded in the body tells you whether origin code ran. The page rendering that hit number proves the browser consumed the expected body. A duration threshold cannot replace any of these signals because a network response can be fast and a cache response can be delayed.
Correlate each fetch independently. Repeated requests share a URL but should have their own request IDs and lifecycle events. The classic WebDriver and BiDi correlation guide provides the right model: browser action, asynchronous event, body rendering, and cleanup are related evidence, not one ordered command result.
Write exclusions in the contract. This article tests the browser's resource cache behavior, not a CDN edge, reverse proxy, service worker Cache Storage, application memory cache, IndexedDB, or server memoization. Those layers may be valuable, but combining them creates a result that cannot assign failure. The first observable boundary is the target responseCompleted event; the release assertion joins it with the origin count and rendered body.
Map the Cache Layers and Choose Controls
A browser fetch can cross several caches before reaching application code. The same visible data can come from local HTTP cache, a service worker, a CDN, a gateway, an origin cache, or a JavaScript singleton. Build a controlled route that bypasses unrelated layers or gives each layer a separate marker. Never infer the source from response speed alone.
| Question | Primary control | Evidence | Negative control |
|---|---|---|---|
| Did the browser reuse a local HTTP response? | Same URL twice under DEFAULT | isFromCache, request IDs, unchanged origin hit | Unique cold URL |
| Does browser cache bypass work? | Warm URL under BYPASS | isFromCache=false, increased origin hit | Same URL under DEFAULT |
| Is a response intentionally uncacheable? | Cache-Control: no-store fixture | Two non-cache events and two origin hits | Cacheable sibling route |
| Does the cache key honor a variant? | Controlled query or Vary input | Separate body marker and origin count per key | Identical-key warm fetch |
| Is a service worker serving the resource? | Dedicated worker fixture and registration evidence | Worker marker plus Cache Storage assertions | Unregistered scope |
Use event subscriptions for observation. A cache test does not normally need a blocking network intercept. Adding a responseStarted or beforeRequestSent intercept changes the lifecycle and introduces a terminal-continuation obligation that can be mistaken for cache latency. If another requirement needs interception, put it in a separate test with its own intercept ID, phase, decision ledger, and removal.
The response-body capture article is useful when body bytes must be inspected, but a cache fixture can usually expose a small origin counter in its JSON. The network authentication guide also matters when private responses are involved: do not assume an authenticated response is safely reusable across accounts or user contexts.
Prefer a fresh browser session and unique URL over an attempt to clean every browser cache. A UUID query value creates a cold key without claiming that all caches are empty. Keep the resource same-origin with the fixture page, return explicit cache headers, and exclude it from service-worker scope and CDN caching. The control endpoint used for diagnostics should return no-store so its own state is never stale.
Verify setCacheBehavior and Event Support
The Java code below targets Selenium 4.44.0, checked against its tagged source on July 18, 2026. That binding exposes Network.setCacheBehavior(CacheBehavior), a context-list overload, CacheBehavior.DEFAULT, CacheBehavior.BYPASS, onResponseCompleted, and ResponseData.isFromCache(). Pin the dependency because the network module continues to evolve and documentation examples can trail released signatures.
Enable BiDi through the webSocketUrl capability before driver creation. The official Selenium BiDi overview explains the WebSocket session channel. A remote Grid must carry that channel as well as classic commands. Before cache cases run, use a capability probe that receives one response event from a known route and records browser, driver, client, and provider versions.
The official Selenium W3C network page documents Java response-event access. The WebDriver BiDi network specification defines cache behavior values as default and bypass, allows context scoping, and describes bypass behavior through implementation-specific resource caches. That wording means cross-browser verification is required; it is not permission to average incompatible results together.
DEFAULT means the caches normally enabled by the remote end configuration. It does not mean empty, memory-only, disk-only, or identical behavior across local and Grid browsers. BYPASS does not delete stored entries. Restoring DEFAULT after a test re-enables ordinary policy, so any requirement for an empty store still needs a fresh session, fresh user context, browser-specific supported clearing mechanism, or unique cache key.
The high-level Selenium network features documentation emphasizes browser networking use cases but may not show every Java cache command. Verify the installed artifact directly, keep a compile probe in the project, and avoid a reflective call that converts an unsupported API into a late runtime branch. Unsupported combinations should be reported, not counted as passing cache behavior.
Build a Fixture with Origin-Backed Evidence
Create a route such as /api/cacheable?case=<uuid>. On each origin execution, increment a case-scoped counter and return JSON containing caseId, originHit, and stable synthetic content. Add Cache-Control: public, max-age=600, a deterministic content type, and no Vary field unless cache-key variation is the scenario. Exclude this route from CDN storage and service-worker interception.
The browser page should fetch that exact URL only when a button is clicked. It renders the body-provided originHit, content marker, and a local render count. The first click should show origin hit 1. A warm cache hit should still show origin hit 1 because the reused body contains the first value. A bypass request should reach the origin and render hit 2. This body sequence provides evidence without a second diagnostic request racing the target.
Provide a sibling /api/no-store route with the same response shape and Cache-Control: no-store. It should increment on every fetch and never report a local cache response. A third route can vary by a benign request field when testing Vary, but keep that scenario separate from the basic warm/bypass case.
Use synthetic data only. A private profile response can reveal a severe cache-key defect, but it should be tested with dedicated accounts, permissions, and redacted artifacts. The foundational mechanism test needs no customer data, credentials, or production origin. Record the case UUID rather than a full URL containing sensitive parameters.
Cacheability can be invalidated by cookies, authorization, validators, browser fetch options, developer settings, or response headers. Make all preconditions visible in the fixture contract. A test that passes only because a framework silently sets cache: "no-store" is not testing normal browser caching. Inspect the request and response names needed for diagnosis, but avoid retaining entire header values.
Execute the Cold, Warm, and Bypass Workflow
Use this sequence so each transition has an independent assertion:
- Generate a unique case ID and start a BiDi-enabled browser with no service worker controlling the fixture scope.
- Open the page, create a context-scoped
Network, and subscribe to targetresponseCompletedevents before the first fetch. - Set that top-level context to
CacheBehavior.DEFAULTand fetch the unique resource once. - Assert a non-cache response, origin hit 1, expected body content, and the first request ID.
- Fetch the identical URL again without changing request inputs or browser context.
- Assert a local-cache response, a different request ID, unchanged origin hit 1, and the same body content.
- Set the context to
CacheBehavior.BYPASS, fetch again, and assert a non-cache response with origin hit 2. - Restore
DEFAULTinfinally, close the event module, and quit the driver last.
Java example 1: normalize bounded cache evidence
import java.net.URI;
import org.openqa.selenium.bidi.network.ResponseDetails;
record CacheEvidence(
String requestId,
String contextId,
long redirectCount,
int status,
boolean fromCache,
String originAndPath) {
static CacheEvidence from(ResponseDetails event) {
URI uri = URI.create(event.getRequest().getUrl());
return new CacheEvidence(
event.getRequest().getRequestId(),
event.getBrowsingContextId(),
event.getRedirectCount(),
event.getResponseData().getStatus(),
event.getResponseData().isFromCache(),
uri.getScheme() + "://" + uri.getAuthority() + uri.getPath());
}
}This record intentionally omits query values and headers from general artifacts. Keep the case ID in a separate known field, or retain a digest when the event must be joined to server logs. Bound the number of records per test and filter before enqueueing; a page can generate hundreds of unrelated subresource events.
Java example 2: prove cold, warm, and bypass behavior in one context
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.time.Duration;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.bidi.module.Network;
import org.openqa.selenium.bidi.network.CacheBehavior;
import org.openqa.selenium.bidi.network.ResponseDetails;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.support.ui.WebDriverWait;
class BrowserCacheTest {
@Test
void distinguishesColdWarmAndBypassResponses() throws Exception {
String caseId = UUID.randomUUID().toString();
String resourceUrl = "https://app.example.test/api/cacheable?case=" + caseId;
ChromeOptions options = new ChromeOptions();
options.setCapability("webSocketUrl", true);
WebDriver driver = new ChromeDriver(options);
try {
driver.get("https://app.example.test/cache-lab?case=" + caseId);
String contextId = driver.getWindowHandle();
BlockingQueue<ResponseDetails> responses = new LinkedBlockingQueue<>(4);
try (Network network = new Network(contextId, driver)) {
network.onResponseCompleted(event -> {
if (resourceUrl.equals(event.getRequest().getUrl())) {
responses.offer(event);
}
});
network.setCacheBehavior(CacheBehavior.DEFAULT, List.of(contextId));
try {
ResponseDetails cold = fetchOnce(driver, responses, 1, "1");
ResponseDetails warm = fetchOnce(driver, responses, 2, "1");
assertFalse(cold.getResponseData().isFromCache());
assertTrue(warm.getResponseData().isFromCache());
assertNotEquals(
cold.getRequest().getRequestId(), warm.getRequest().getRequestId());
assertEquals(200, warm.getResponseData().getStatus());
network.setCacheBehavior(CacheBehavior.BYPASS, List.of(contextId));
ResponseDetails bypass = fetchOnce(driver, responses, 3, "2");
assertFalse(bypass.getResponseData().isFromCache());
} finally {
network.setCacheBehavior(CacheBehavior.DEFAULT, List.of(contextId));
}
}
} finally {
driver.quit();
}
}
private static ResponseDetails fetchOnce(
WebDriver driver,
BlockingQueue<ResponseDetails> responses,
int expectedRenderCount,
String expectedOriginHit) throws Exception {
driver.findElement(By.id("fetch-cacheable")).click();
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(5));
wait.until(d -> d.findElement(By.id("render-count")).getText()
.equals(String.valueOf(expectedRenderCount)));
wait.until(d -> d.findElement(By.id("origin-hit")).getText()
.equals(expectedOriginHit));
ResponseDetails response = responses.poll(5, TimeUnit.SECONDS);
assertNotNull(response, "Expected a responseCompleted event");
return response;
}
}The bounded queue prevents unbounded event retention, while exact URL filtering prevents the page document and control routes from entering the assertion stream. In production code, publish callback failures separately because a listener exception may otherwise appear as a missing event. The fixture should also assert stable body content, not only originHit, so a cache key cannot return another case's data unnoticed.
Correlate Events Without Introducing an Intercept
Every fetch should produce its own evidence row: case ID, context, request ID, redirect count, sanitized target, response status, fromCache, rendered origin hit, and render count. Join on request ID for browser events and on case ID for origin state. Do not use the case query as the only event key because retries or multiple clicks can share it.
The listener lifecycle starts before the cold request and ends after cache mode restoration. No blocking intercept is required, so isBlocked should remain false for these events. If a shared framework has an active intercept, fail setup or run in a separate session. A blocked event requires a continuation and can change response timing, headers, or body flow, invalidating the simple cache claim.
The second request ID should differ from the first even though the URL and body match. If only one event appears, determine whether browser event semantics, application memoization, or a service worker suppressed the second network lifecycle. The DOM result alone cannot tell you. The browsing-context event guide helps when navigation or a popup moves the fetch into another owner.
Redirects should be absent from the base fixture. If the application requires them, record each redirect count and define which response may be cached. A redirect response and final response have different policies. Do not flatten them into one URL-based result. Keep status and final body assertions alongside cache state.
When migrating a CDP implementation, compare normalized fields and behavior in separate browser sessions. The CDP to BiDi migration article is useful for preserving the old cold/warm/bypass contract while replacing vendor-specific cache commands. Do not demand identical raw payloads when the stable proof is local cache state plus origin execution.
Add Negative Controls for Cache Keys and Isolation
The first negative control is a unique cold URL. It must report fromCache=false and origin hit 1. The second is a no-store sibling fetched twice; both requests should bypass local reuse and increment origin state. These controls catch a hard-coded true, stale listener queue, or page-level memoization that returns data without issuing a second fetch.
Test key separation with two case IDs in one browser. Warming A must not make the first fetch for B a cache hit or return A's body. If the server uses Vary, create a dedicated matrix for the varied field: same field should reuse, changed field should not. Do not combine query-key and Vary behavior in one assertion because a failure cannot identify which key dimension was wrong.
Private data needs cross-user negative coverage. A response for account A must never appear for account B, even when the URL path matches. Use synthetic accounts and an isolated environment, and assert body identity as well as cache metadata. Do not store raw profile data in artifacts. Cache poisoning and data leakage should block immediately rather than enter a retry policy.
Service workers require an explicit negative check. Confirm navigator.serviceWorker.controller is absent in the simple fixture or serve the page outside any registered scope. If worker caching is the target, create a separate suite with registration version, worker state, Cache Storage key, and message evidence. The browser HTTP cache command should not be claimed as a universal web-cache switch.
Cross-browser differences belong in a declared compatibility matrix. The CDP and BiDi parity testing guide helps normalize request identity, response status, and cache flags while retaining browser-specific diagnostics. A browser that cannot provide the required event or cache control is unsupported for this gate, not automatically passing.
Diagnose False Hits, Misses, and Missing Events
A false warm miss often comes from response policy rather than BiDi. Inspect the allowlisted names Cache-Control, Expires, ETag, Vary, Age, and request cache mode without dumping sensitive values. Check whether the URL changed, a cookie varied the response, authorization disabled reuse, the server emitted no-store, or the page used a fetch option that bypasses cache.
A false hit may be an event-queue bug. Ensure each fetch consumes exactly one target event and compare request IDs. Clear no shared queues between cases because there should be no shared queue at all. A response with the wrong case body indicates cache-key contamination, CDN behavior, proxy reuse, or application memoization and must not be accepted merely because isFromCache matches expectation.
Missing events require layer-by-layer diagnosis: BiDi capability, listener registration, context scope, exact URL filter, browser trigger, service worker ownership, and provider transport. The WebSocket subscription article gives a useful activation and closure checklist. Preserve the last known lifecycle marker instead of raising all timeouts together.
If BYPASS still reports a cache hit, verify that the command was applied to the top-level context that owns the fetch and completed before the click. Compare behavior with session-wide setCacheBehavior in a diagnostic run, but keep the production scope narrow. Record browser and binding versions because the protocol leaves some resource-cache implementation details to the browser.
Origin hit mismatches can arise outside the browser. A CDN or reverse proxy may satisfy a non-local request without executing origin code. Disable it for the fixture or add a trusted edge marker so the result can distinguish local browser, edge, and origin. Do not relabel an edge hit as a browser-cache hit.
Restore State and Operate the CI Gate
Cache behavior is mutable session state. The test that changes it owns restoration even when an assertion fails. Call setCacheBehavior(DEFAULT, List.of(contextId)) in finally, then close Network to clear listeners, then quit the browser. Closing the module does not itself promise to reset cache behavior, and returning to DEFAULT does not remove stored entries.
Use a fresh session per parallel test when practical. At minimum, use isolated user contexts and unique keys, but verify the provider supports the intended context behavior. Never let two workers share an origin counter. A UUID should identify one case from fixture creation through event records and artifact names, then the server fixture should expire it after the run.
The Selenium BiDi browser cache testing CI pipeline should pin Selenium, browser, driver or Grid image, operating system image, and provider settings. Run a small deterministic cache lane on relevant dependency or browser changes. Classify capability, server fixture, browser-cache, service-worker, intermediary, assertion, and cleanup failures separately so the first owner is obvious.
Do not retry an unexplained cache result. A fresh-session retry can erase the state that made the defect visible. Retries are reserved for classified startup or transport failures and must retain the first attempt. Gate on cross-case body leakage, unexpected local reuse under bypass, stale no-store data, missing correlation, wrong context, or failed state restoration.
Publish a compact artifact with versions, case ID, context, request IDs, redirect counts, cache behavior at each trigger, fromCache values, rendered origin hits, control results, and cleanup status. The Selenium BiDi interview guide offers useful prompts for checking whether reviewers can distinguish browser cache evidence from CDN, worker, and application cache evidence.
Frequently Asked Questions
What does ResponseData.isFromCache prove?
It reports whether the browser's response cache state is local for that network response. Correlate it with the request ID, a unique resource URL, and origin-backed evidence. It does not by itself distinguish memory from disk cache, prove CDN behavior, or rule out a service worker response.
Does CacheBehavior.BYPASS clear the browser cache?
No. BYPASS tells the browser to bypass applicable implementation-specific resource caches for matching requests. It is not a cache-deletion command. Previously stored entries may remain available after behavior returns to DEFAULT, so use unique URLs or a fresh context when a test requires known empty cache state.
How do I create a reliable warm-cache test fixture?
Use a unique same-origin URL whose server returns explicit cacheable headers, a stable body, and an origin hit number inside that body. Fetch it twice in one browser context without a service worker. Require a cold first response, a local-cache second response, and an unchanged origin hit number.
Why can a service worker invalidate a cache assertion?
A service worker can answer a fetch from its own Cache Storage or synthesized logic, which is a different layer from the browser HTTP cache controlled by this test. Disable service workers in the fixture, use a clean scope, or test service-worker caching separately with its own observable contract and controls.
What cleanup does a cache-behavior test own?
Restore the affected context to CacheBehavior.DEFAULT in finally, close the Network module to remove listeners, and quit the browser last. DEFAULT restores normal cache policy but does not erase entries. Keep cache mode, context ID, and cleanup result in the case evidence so later tests cannot inherit hidden behavior.
How should cache tests run in parallel CI?
Give each worker a fresh browser session or isolated user context, unique cache key, dedicated origin counter, and bounded event queue. Pin Selenium and browser versions, disable unrelated intermediaries, and separate browser-cache failures from server, CDN, service-worker, and Grid failures. Do not retry away an unexplained cache hit.
Practice a Cache Experiment in QABattle
Open the QABattle battle catalog and choose a browser workflow with a repeatable read. Build a unique cacheable fixture, then submit cold, warm, bypass, and no-store observations with request IDs and origin-backed content. State which layer each assertion covers and which layers are intentionally excluded.
Review the timeline with the command-event correlation article, then challenge artifact retention using the response-body evidence guide. A strong solution does not celebrate a fast second request. It proves local reuse, origin avoidance, isolation, bypass behavior, and owned restoration with evidence another engineer can reproduce.
// LIVE COURSE / THE TESTING ACADEMY
Playwright Automation Mastery
Go beyond Selenium. Master Playwright with JS/TS in 90 days.
From the instructor behind this guide.
Playwright jobs are growing 8x faster than Selenium. 90 days / 75+ live hrs / Tue-Thu-Sat 7 AM IST.
PRIMARY REFERENCES
Verify the details at the source
QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.
- 01Official selenium.dev reference
selenium.dev
Primary documentation selected and verified for the claims in this guide.
- 02Official selenium.dev reference
selenium.dev
Primary documentation selected and verified for the claims in this guide.
- 03Official selenium.dev reference
selenium.dev
Primary documentation selected and verified for the claims in this guide.
- 04Official w3c.github.io reference
w3c.github.io
Primary documentation selected and verified for the claims in this guide.
FAQ / QUICK ANSWERS
Questions testers ask
What does ResponseData.isFromCache prove?
It reports whether the browser's response cache state is local for that network response. Correlate it with the request ID, a unique resource URL, and origin-backed evidence. It does not by itself distinguish memory from disk cache, prove CDN behavior, or rule out a service worker response.
Does CacheBehavior.BYPASS clear the browser cache?
No. BYPASS tells the browser to bypass applicable implementation-specific resource caches for matching requests. It is not a cache-deletion command. Previously stored entries may remain available after behavior returns to DEFAULT, so use unique URLs or a fresh context when a test requires known empty cache state.
How do I create a reliable warm-cache test fixture?
Use a unique same-origin URL whose server returns explicit cacheable headers, a stable body, and an origin hit number inside that body. Fetch it twice in one browser context without a service worker. Require a cold first response, a local-cache second response, and an unchanged origin hit number.
Why can a service worker invalidate a cache assertion?
A service worker can answer a fetch from its own Cache Storage or synthesized logic, which is a different layer from the browser HTTP cache controlled by this test. Disable service workers in the fixture, use a clean scope, or test service-worker caching separately with its own observable contract and controls.
What cleanup does a cache-behavior test own?
Restore the affected context to CacheBehavior.DEFAULT in finally, close the Network module to remove listeners, and quit the browser last. DEFAULT restores normal cache policy but does not erase entries. Keep cache mode, context ID, and cleanup result in the case evidence so later tests cannot inherit hidden behavior.
How should cache tests run in parallel CI?
Give each worker a fresh browser session or isolated user context, unique cache key, dedicated origin counter, and bounded event queue. Pin Selenium and browser versions, disable unrelated intermediaries, and separate browser-cache failures from server, CDN, service-worker, and Grid failures. Do not retry away an unexplained cache hit.
RELATED GUIDES
Continue the learning route
GUIDE 01
Selenium WebDriver BiDi Interview Questions
Selenium BiDi interview questions advanced: practical design, implementation, debugging, CI, metrics, and interview guidance for QA, SDET, and automation engineers.
GUIDE 02
Capture Network Response Bodies with Selenium WebDriver BiDi
Master Selenium BiDi response body with practical examples, architecture decisions, failure analysis, CI guidance, metrics, and scenario-led interview answers.
GUIDE 03
Migrate Selenium CDP Hooks to WebDriver BiDi
Migrate Selenium CDP hooks to WebDriver BiDi with a capability inventory, event adapters, parity tests, cross-browser rollout, and failure diagnostics.
GUIDE 04
Compare CDP and WebDriver BiDi Event Parity
A practical guide to Selenium CDP BiDi event parity testing, with implementation examples, debugging workflows, CI evidence, security controls, and release gates.
GUIDE 05
Combine Classic WebDriver Commands with BiDi Events
Combine Classic WebDriver Commands with BiDi Events: practical implementation, debugging, evidence, security, CI, and release guidance for QA teams.