When Playwright Fails After a Refresh, Suspect Browser State Before You Suspect the Test
By David Frei · October 29, 2026
A practical Playwright debugging guide for failures caused by browser cache, service workers, or IndexedDB state that survives navigation and refresh, with isolation steps and code examples.
A Playwright test that passes on a fresh page load and fails only after a refresh is usually telling you something useful: the bug is in state that survives navigation, not in the click or assertion you wrote last. That state is often one of three things, browser cache, a service worker, or IndexedDB. They behave differently, they fail differently, and they need different isolation steps.
If you treat all three as “cache”, you end up clearing the wrong layer, hiding the symptom, or rewriting the test around the real product defect. The faster path is to identify which persistence layer is involved, prove it locally, and then decide whether to clear state in the test or redesign the scenario.
The short version
When a Playwright test only fails after refresh, I would debug in this order:
- Check whether the app is reading stale network responses. That points to browser cache or a service worker.
- Check whether client-side state survives reload. That points to IndexedDB, localStorage, sessionStorage, or cookies.
- Use a blank browser context to remove cross-test contamination.
- Disable or bypass the service worker if the app can do it safely.
- Inspect IndexedDB contents directly before and after refresh.
The useful question is not “how do I make Playwright ignore refresh?” It is “which browser persistence layer is changing the app’s post-refresh behavior?”
Playwright gives you enough browser-control surface to isolate this without guessing. The main reference points are the Playwright docs, browser contexts, and storage state handling.
First, separate the three persistence layers
These three are easy to blur together, but they are not the same.
Browser cache
Browser cache is the HTTP cache used by the browser for network resources. A request may never reach the server if the browser considers a cached response valid. In debugging, the clue is that the app changes behavior even though the UI code did not change and the network tab shows fewer requests than expected.
Service worker
A service worker is a script that runs separately from the page and can intercept network requests, cache assets, and serve responses even after a refresh. If your app uses one, a refresh does not mean a clean network path. For app shells, offline support, or aggressive caching, this is often the first place I look.
IndexedDB
IndexedDB is a persistent client-side database. It survives refreshes and is designed for structured browser storage. If a test fails only after the app rehydrates state from IndexedDB, the page may be loading correctly but starting from stale application data.
The distinction matters because the fix is different:
- Browser cache problems often need request-control or fresh contexts.
- Service worker problems often need bypassing, unregistering, or clearing service worker state.
- IndexedDB problems often need a better test setup, explicit data seeding, or a real reset path in the app.
A practical decision table
| Symptom | Likely layer | What to log first | Typical fix |
|---|---|---|---|
| Refresh changes what data the page shows, and network requests seem skipped | Service worker or browser cache | Network requests, service worker registration, response sources | Bypass or unregister service worker, fresh context |
| App behaves differently only after reload, but the URL and DOM structure are the same | IndexedDB or other client storage | IndexedDB contents, localStorage, cookies | Clear state, seed deterministic data, or reset through the app |
| First load passes, reload fails with the same page state | Service worker, IndexedDB, or cache interaction | Request logs before and after reload | Compare cold vs warm load paths |
| Failure appears only in CI, not locally | Shared browser profile, retained context, or test ordering | Context reuse, storage state, worker registration | Use isolated contexts and explicit cleanup |
Start with a clean test boundary
Before debugging the product, make sure the test itself is not reusing state across cases. In Playwright, a fresh browser context is the clean boundary you want for most tests.
import { test, expect } from '@playwright/test';
test('starts from a clean context', async ({ browser }) => {
const context = await browser.newContext();
const page = await context.newPage();
await page.goto('https://example.com');
await expect(page).toHaveTitle(/Example/);
await context.close();
});
If you are already using the built-in page fixture, the important question is whether your suite is also reusing storageState, a persistent context, or a browser profile across tests. That can be intentional, but it makes refresh-related failures much harder to reason about.
A simple isolation rule helps:
- Use a fresh context when debugging a persistence bug.
- Reintroduce shared state only after you know exactly which storage layer the app needs.
Log the right evidence before changing the test
When a failure is intermittent, I want evidence from both sides of the refresh: before reload and after reload. The useful signals are not screenshots alone, but requests, storage state, and app-level markers.
1. Capture network requests and responses
If the page depends on a service worker or cache, the request path often changes after refresh. Add request logging around the failing flow.
page.on('request', request => {
console.log('>>', request.method(), request.url());
});
page.on(‘response’, async response => { if (response.request().resourceType() === ‘document’ || response.request().resourceType() === ‘xhr’) { console.log(‘«’, response.status(), response.url()); } });
You are looking for patterns such as:
- Requests disappearing after reload.
- A document or API response returning unexpectedly from cache.
- A service worker serving stale data while the server has newer content.
2. Inspect storage before and after refresh
For IndexedDB debugging, compare the state before and after refresh. Playwright does not give you a magical product-level diagnosis here, but it does let you evaluate page-side JavaScript.
const dbNames = await page.evaluate(async () => {
return await indexedDB.databases();
});
console.log(dbNames);
If the app uses a known database name, inspect the object stores or at least verify whether the database exists after refresh. The key question is whether the application state is being restored from an old record, not whether the page is loading.
3. Check whether a service worker is registered
A registered service worker can change the entire failure shape. If you suspect one, inspect registrations from the page.
const registrations = await page.evaluate(async () => {
return await navigator.serviceWorker.getRegistrations();
});
console.log(registrations.map(r => ({ scope: r.scope, active: !!r.active })));
If a service worker is active and the bug only appears after a refresh, that is a strong signal that the worker is participating in the failure.
How to isolate browser cache from service worker behavior
Browser cache and service worker cache are related, but not identical. That distinction matters because a service worker can return a cached response even when the browser HTTP cache is not the main factor.
A good isolation sequence is:
- Run the test in a fresh context.
- Disable or bypass the service worker if the app supports it.
- Repeat the flow with cache behavior altered only one layer at a time.
If your app allows it, a debug build that unregisters the service worker is often the cleanest path. If not, at least add a one-time diagnostic path that shows whether the app is under service worker control.
Do not clear everything first. If the test passes only after you wipe all client storage, you still do not know whether the real cause was cache, service worker, or IndexedDB.
For API-heavy apps, I also watch whether the refresh changes request headers or response timing. A service worker can short-circuit what looks like a normal navigation, which makes the UI appear “flaky” when the actual problem is deterministic stale state.
How to debug IndexedDB-specific flakiness
IndexedDB issues usually show up when the app boot sequence reads persisted records and then renders a different view than the one your test expects. The failure often appears after a refresh because the first load path and the hydrated load path are not the same.
The debugging flow I trust is:
- Confirm the app writes to IndexedDB before refresh.
- Confirm the app reads from IndexedDB during startup.
- Compare the records before and after refresh.
- Check whether the test data is unique enough to avoid stale reads.
If the app uses IndexedDB for queued actions, auth bootstrap, draft state, or offline sync, a refresh can reapply old records exactly as designed. In that case, clearing the database inside the test may hide a product issue rather than solve a test issue.
A better fix is often one of these:
- Seed the database with known data through an API or setup endpoint.
- Expose a dedicated test reset action in the app.
- Make the test assert the app’s recovery behavior instead of assuming a blank client.
When to clear state, and when not to
There is a real difference between a test workaround and a product requirement.
Clear state when
- The test is only verifying a single user journey and does not care about persistence.
- You are reusing a browser context for speed, but the test should be isolated.
- The app under test is not supposed to preserve the state being exercised.
Redesign the test or app state flow when
- The product is intentionally persistence-driven, such as drafts, offline queues, or cached content.
- The bug only appears because a setup step is unrealistic, such as deleting storage in the middle of a flow that real users never experience.
- You need a reliable regression test for recovery after refresh, not a one-off pass condition.
A blunt state wipe can make the suite greener while reducing its diagnostic value. If the persistence behavior is part of the feature, your test should model it.
A repeatable local repro recipe
When I need to reproduce this class of bug locally, I keep the steps boring and explicit:
- Run the test in headed mode so you can see the post-refresh transition.
- Use a fresh browser context, not an existing session.
- Add request and response logging.
- Dump service worker registrations.
- Inspect IndexedDB before and after refresh.
- Compare the failing run with a manually reloaded browser window.
If the failure disappears in headed mode, do not assume the bug is gone. It may just be timing-sensitive. Make the state visible before changing the assertions.
What I would change in the test suite
For a suite that repeatedly hits this problem, I would make three structural changes:
- Separate setup from behavior. Seed data in a dedicated helper, not inline in the failing test.
- Prefer clean browser contexts. Reuse only the state you intentionally need.
- Treat refresh as a first-class case. If refresh is part of the user journey, test it explicitly instead of relying on incidental reloads.
Playwright is good at expressing those boundaries because browser contexts, page events, and storage manipulation are all first-class concepts in the framework documentation. That makes it easier to explain why a test is failing, which is the point of debugging in the first place.
A simple rule for choosing the fix
If the failure goes away when you disable the service worker, focus on service worker behavior first.
If the failure goes away when you clear IndexedDB, focus on persistence and seeding first.
If the failure goes away only in a fresh context, focus on test isolation first.
That sequence saves time because it narrows the problem to one persistence layer instead of treating refresh as a generic flake.
FAQ
Is browser cache the same as a service worker cache?
No. Browser cache is the browser’s HTTP cache, while a service worker can intercept requests and return its own cached responses. They can overlap in symptoms, but they are different debugging targets.
Should I disable service workers in all Playwright tests?
No. Disable or bypass them only when the test does not need service worker behavior. If the application depends on offline support or cached startup paths, you should test that behavior explicitly.
Can Playwright clear IndexedDB automatically between tests?
Not in a way that solves every persistence problem. The more reliable pattern is to use fresh browser contexts and explicit app-level reset or seed logic when the test requires persisted data.
Why does a test pass before refresh and fail after refresh?
Because refresh can switch the app onto a different startup path, such as restoring state from IndexedDB, serving assets through a service worker, or reading stale responses from cache.
What is the fastest first check for this kind of flake?
Log network requests and service worker registrations, then inspect IndexedDB contents before and after refresh. That usually tells you which persistence layer is involved before you touch the assertions.
Bottom line
If a Playwright test only fails after refresh, do not start by rewriting waits. Start by identifying what survives the reload. Browser cache, service workers, and IndexedDB each create a different kind of persistence bug, and each one needs a different fix.
The most reliable debugging path is simple, but disciplined: isolate the browser context, log requests, inspect service workers, compare IndexedDB state, and then decide whether the right answer is to clear state or model persistence deliberately. That keeps the test honest and makes the failure useful instead of mysterious.