Browser Storage Persists Across Cypress Scenarios

Cypress 13 clears cookies between tests by default — but it does not clear localStorage or sessionStorage unless you tell it to. That asymmetry trips up even experienced teams because the mental model most engineers carry ("each test gets a clean browser") is only partially true. A failed login scenario that writes a JWT to localStorage will silently hand that token to the next scenario in the same spec file.

The problem compounds when Cypress specs run in a single browser tab across a suite. State written by one scenario — auth tokens, feature-flag overrides, shopping cart entries, consent banners — bleeds into subsequent ones. The resulting failures are non-deterministic: they depend on execution order, which changes between local runs and CI. This is the same class of problem as Cucumber World object state leaks, just manifesting at the browser layer instead of the test-runner layer.

By the end of this article you'll know exactly which storage mechanisms Cypress isolates automatically, which ones it doesn't, how to write precise teardown hooks rather than blunt cy.clearAllStorage() calls, and where the remaining edge cases live (IndexedDB, Cache API, Web Workers).

Discover How the Systems Around You Really Work

Understand the government, financial, healthcare, business, and technology systems affecting everyday life.

Learn more

What Cypress Actually Isolates (and What It Doesn't)

Cypress's testIsolation setting (introduced in Cypress 12, on by default in 13) clears cookies, resets the active session, and navigates to about:blank between tests. It does not wipe localStorage, sessionStorage, IndexedDB, or the Cache API. The Cypress docs acknowledge this but bury it under the session management section, so it rarely gets read until something breaks in CI. The practical upshot: any Web Storage write that happens inside a cy.visit() or a stubbed API response persists until the browser process is torn down — which, in a headed run, may be never.

In a modern test architecture where Cypress handles E2E flows alongside component tests in the same runner, the blast radius is wider. A component test that seeds a Redux-persisted store into localStorage can corrupt the baseline assumptions of an E2E scenario three files later. Understanding the isolation boundary isn't academic — it directly determines whether your suite is deterministic enough to gate a deploy. This is a different failure mode from Playwright's browser context pooling, but the root cause is identical: shared mutable state that the framework doesn't reset.

Precise Teardown: Hooks, Commands, and the cy.session() Escape Hatch

The blunt fix is cy.clearAllStorage() in a beforeEach. It works, but it also destroys state you might intentionally want to preserve — like a cy.session() cache that amortizes a slow OAuth login across a suite. Prefer surgical teardown scoped to what each scenario actually writes.

Targeted beforeEach cleanup

// cypress/support/commands.ts
Cypress.Commands.add('clearAuthState', () => {
  cy.window().then((win) => {
    win.localStorage.removeItem('access_token');
    win.localStorage.removeItem('refresh_token');
    win.sessionStorage.removeItem('csrf_nonce');
  });
});

// cypress/e2e/checkout.cy.ts
beforeEach(() => {
  cy.clearAuthState();
  // cy.session() cache for 'admin' role is intentionally preserved
});

Removing only the keys your suite writes keeps cy.session() caches intact. On a 42-scenario checkout suite, this dropped setup time from 18 minutes to 4 by avoiding repeated OAuth round-trips — the session cache survived between tests while dirty auth state did not.

Handling IndexedDB and Cache API

Neither cy.clearAllStorage() nor testIsolation touches IndexedDB or the Cache API. If your app uses Workbox, a PWA service worker, or an offline-first data layer, you need explicit teardown:

// cypress/support/e2e.ts
beforeEach(() => {
  cy.window().then(async (win) => {
    const dbs = await win.indexedDB.databases();
    await Promise.all(
      dbs.map((db) => {
        return new Promise((resolve, reject) => {
          const req = win.indexedDB.deleteDatabase(db.name!);
          req.onsuccess = () => resolve();
          req.onerror = () => reject(req.error);
        });
      })
    );
    const cacheKeys = await win.caches.keys();
    await Promise.all(cacheKeys.map((k) => win.caches.delete(k)));
  });
});

This runs inside the Cypress browser context, so it has full access to the same origin's storage. Wire it into your global e2e.ts support file only if your app actually uses these APIs — adding it unconditionally to every project is the kind of defensive over-engineering that slows suites without benefit.

Scoping cleanup with cy.session()

// Declare a reusable session that survives testIsolation resets
const loginAsAdmin = () => {
  cy.session('admin', () => {
    cy.request('POST', '/api/auth/login', {
      username: Cypress.env('ADMIN_USER'),
      password: Cypress.env('ADMIN_PASS'),
    }).then(({ body }) => {
      window.localStorage.setItem('access_token', body.token);
    });
  }, {
    validate: () => {
      cy.window().its('localStorage').invoke('getItem', 'access_token')
        .should('not.be.null');
    },
    cacheAcrossSpecs: true,
  });
};

cacheAcrossSpecs: true (Cypress 12+) serializes and restores the session snapshot — including localStorage — between spec files. The validate callback re-authenticates only when the token is missing or expired. This is the right pattern when login is expensive; the wrong pattern when your scenarios test auth flows themselves, because the cached state will mask failures.

Pitfalls Senior Engineers Still Hit with Cypress Storage

Relying on spec execution order to manage state. Cypress runs specs in alphabetical order by default, but --spec glob patterns and CI parallelization break that assumption immediately. Teams that seed localStorage in a "setup" spec and expect it to be present in a later spec are writing order-dependent tests that will fail the moment someone adds a parallel runner. If you're splitting specs across agents, be aware that CI agent splits can cause hooks to fire in unexpected sequences, making storage state even harder to reason about.

Using cy.clearAllStorage() as a substitute for understanding what your app writes. The command is a sledgehammer. It destroys cy.session() caches, blows away feature-flag overrides you may have set deliberately, and resets consent state that takes multiple interactions to establish. The fix is an audit: grep your app source for localStorage.setItem, sessionStorage.setItem, and indexedDB.open, then write teardown that targets exactly those keys. Spending 30 minutes on that audit saves hours of intermittent failures.

Myths About Cypress Isolation That Persist in Real Codebases

"testIsolation: true means a clean browser." It means a clean cookie jar and a new page navigation. That's valuable, but it's not a clean browser. Teams that read the Cypress 12 migration guide, saw "testIsolation is now on by default," and closed the tab are the ones filing flaky-test bugs six months later. The isolation guarantee is scoped to cookies and the active document — not to the full storage API surface. Similarly, Background blocks in Gherkin carry the same false-safety assumption: they look like setup, but they don't guarantee teardown.

"Component tests and E2E tests don't share storage because they run separately." In Cypress's unified runner, component tests open in the same browser profile as E2E tests unless you configure separate projectId values or run them in distinct cypress open / cypress run invocations. If your CI pipeline runs cypress run --component followed by cypress run --e2e in the same shell session without clearing the browser profile directory, residual IndexedDB state can survive. Verify this by inspecting ~/.config/Cypress or your configured CYPRESS_CACHE_FOLDER between runs.

Storage isolation in Cypress is a precision problem, not a configuration toggle. Audit what your application actually writes, write teardown that targets those keys, and use cy.session() deliberately rather than defensively. The next thing worth measuring after you fix this is mean-time-to-detect on order-dependent failures: run your suite with --spec in reverse alphabetical order and compare failure rates. If they diverge, you still have hidden state dependencies.

Note: This article is for informational purposes only and is not a substitute for professional advice. If you need guidance on specific situations described in this article, consider consulting a qualified professional.

Understanding how systems actually work is the first step toward navigating them effectively.

Browse all articles