ScreenshotNeo

BlogHow-to

How to Fix Common cy.session() Issues in Cypress

Fix blank pages, 401s, wrong accounts, missing storage, and cross-spec cache surprises with a reliable Cypress cy.session() setup.

By the ScreenshotNeo team4 October 20268 min read

cy.session() saves and restores cookies, localStorage, and sessionStorage for a matching session ID. It does not load your application page. If testIsolation is enabled, call cy.visit() after cy.session() before using the app. For 401 responses, wrong accounts, or missing storage, verify that login completed, validate the restored authentication, and make the session ID represent every input that changes the resulting state.

This guide follows Cypress’s documented session behavior. Check the current cy.session() API reference against your installed Cypress version, especially when migrating older cookie-preservation code.

1. Start with a reliable session pattern

Keep three responsibilities distinct: setup establishes the session, validate proves it is authenticated, and the test visits the page it needs. Put a login-success assertion in setup so Cypress does not save a session before the login flow has finished.

const username = Cypress.env('username')
const password = Cypress.env('password')

function loginAs(user = username) {
  cy.session(
    ['web-login', user],
    () => {
      cy.visit('/login')
      cy.get('[name="email"]').type(user)
      cy.get('[name="password"]').type(password, { log: false })
      cy.get('button[type="submit"]').click()

      // Prove the login flow finished before Cypress saves browser state.
      cy.location('pathname').should('eq', '/dashboard')
      cy.get('[data-testid="account-menu"]').should('be.visible')
    },
    {
      validate() {
        // Choose an assertion that reliably distinguishes an authenticated session.
        cy.request('/api/me').its('status').should('eq', 200)
      },
    },
  )
}

describe('dashboard', () => {
  beforeEach(() => {
    loginAs()
    // cy.session() restores browser state; it does not load the app route.
    cy.visit('/dashboard')
  })

  it('shows the signed-in user', () => {
    cy.get('[data-testid="account-menu"]').should('contain', username)
  })
})

Replace the routes, selectors, and authenticated endpoint with ones from your application. Store credentials in Cypress environment configuration or your CI secret store; do not hard-code them in a spec. The example suppresses password typing in the command log, but secret handling should also be configured appropriately for your environment.

What setup, validation, and the test each prove

  • Setup: performs login and waits for evidence that the app accepted it. Cypress saves cookies and browser storage when setup completes.
  • Validation: checks authentication both after a new setup and when Cypress restores a cached session. If a restored session fails validation, Cypress reruns setup; a session that fails validation immediately after setup exposes a login/setup problem.
  • Test body: loads the page under test and checks its behavior. A restored browser session is not the same thing as a loaded route.

2. Diagnose the symptom before changing the session

Symptom Likely cause First check
Commands find no page elements after session The page was cleared by test isolation and was never visited again Visit the route after cy.session()
Protected request returns 401 Setup ended before authentication completed, restored data is stale, or validation is inadequate Assert login completion in setup and authentication in validate
Wrong user, role, or tenant appears Two distinct states share one session ID Include every state-changing input in the ID
Cookie or storage key is absent It was not set by the time setup/validation completed, or the app stores state somewhere else Compare saved session data with currently applied data
Session works in a spec but not another or on another CI worker Cache scope is one run on one machine, or specs define reuse inconsistently Check the cacheAcrossSpecs setting and machine/run boundaries

3. Fix commands failing after cy.session()

With testIsolation enabled, Cypress clears the page. A restored session contains browser authentication state, not the rendered application. Cypress’s documented answer is to call cy.visit() after cy.session(); otherwise commands may run against a blank page.

cy.session('user-session', setupLogin, { validate: validateLogin })
cy.visit('/account')
cy.get('[data-testid="account-menu"]').should('be.visible')

Put assertions about the login destination inside setup, where they prove authentication completed during setup. After Cypress restores a cached session, visit the route the test needs. Do not rely on the page that happened to be open when the session was first created.

When testIsolation is false

When testIsolation: false, Cypress does not clear the page before setup, so a visit is not needed solely to reload it after cy.session(). Cypress still clears cookies and storage before setup. Disabling isolation can let one test’s page state affect another, so keep state explicit and avoid treating this setting as a general cure for session problems.

4. Fix 401 errors after restoring a session

A 401 means the request did not authenticate. It does not by itself tell you whether login never finished or a previously valid cached session expired. Make setup prove login succeeded and make validation test an authenticated result:

cy.session(
  ['account-login', username],
  () => {
    cy.visit('/login')
    cy.get('[name="email"]').type(username)
    cy.get('[name="password"]').type(password, { log: false })
    cy.get('button[type="submit"]').click()
    cy.get('[data-testid="account-menu"]').should('be.visible')
  },
  {
    validate() {
      cy.request('/api/me')
        .its('status')
        .should('eq', 200)
    },
  },
)

Use an endpoint or protected page that requires the same authentication mechanism as the test. If validation only checks that a cookie exists, it may pass even when the cookie is expired or rejected by the server. Conversely, do not use an endpoint that is allowed to return 401 for a legitimate signed-in user.

5. Make session IDs unique and safe

Cypress uses the session ID to decide which saved browser state matches a request. If different setup inputs produce different authentication states, encode those inputs in an array or object ID. Cypress deterministically stringifies arrays and objects.

cy.session(
  {
    login: 'password',
    username,
    role,
    tenant,
  },
  setupLogin,
  { validate: validateLogin },
)

Include values such as username, role, tenant, or login method when they alter the resulting state. Do not include a password, access token, or other secret: session IDs appear in the Cypress reporter. Avoid unnecessary changing values too; they create separate cache entries and reduce reuse.

6. Find missing or unexpectedly recreated browser state

Use the Cypress Sessions Instrument Panel and command log to determine whether Cypress created, restored, or recreated the session. Then compare the stored snapshot with what is currently applied:

// Inspect a saved session by its ID (use the same ID shape as cy.session())
cy.then(() => {
  const saved = Cypress.session.getSession(['web-login', username])
  console.log('saved session', saved)
})

// Inspect cookies and storage currently applied in the browser
cy.then(() => {
  const current = Cypress.session.getCurrentSessionData()
  console.log('current session data', current)
})

These helpers are useful for diagnosis; avoid logging sensitive values in CI output. If a cookie or storage attribute is missing, setup or validation may have completed before the application applied it. Wait for a visible authenticated state or a specific storage-setting event/condition before setup ends. Confirm that the app actually uses cookies, localStorage, or sessionStorage for the state you expect; state held only in memory is not among the browser data cy.session() saves.

7. Understand cross-spec cache scope

cacheAcrossSpecs defaults to false. When enabled, Cypress can reuse the session across specs in the same cypress run on the same machine. It is an in-memory run cache: it does not persist to disk, survive a new run, or move to another parallel CI machine.

cy.session(
  ['web-login', username],
  setupLogin,
  {
    cacheAcrossSpecs: true,
    validate: validateLogin,
  },
)

Every spec that reuses that session must make a consistent cy.session() call with the same ID, setup, validation, and cacheAcrossSpecs value. Each parallel machine needs to establish its own session. If a new run or worker starts without cached state, that is expected behavior, not a corrupted cache.

The API history records cacheAcrossSpecs as added in Cypress 10.9.0, setup as required in 11.0.0, and experimentalSessionAndOrigin as removed when the command became available by default in 12.0.0. Cypress removed Cypress.Cookies.defaults and Cypress.Cookies.preserveOnce; cy.session() is the documented replacement for preserving cookies and browser storage. See the Cypress migration guide when updating older tests.

Cookie commands use the hostname rather than superdomain by default. If a test expects cookies to be shared across subdomains, review the cookie command’s explicit domain option and verify the app’s cookie domain behavior. See the Cypress cookie commands reference.

9. Troubleshooting checklist

  1. Name the failure: blank page, 401, wrong identity, missing storage, or cross-spec reuse.
  2. Read the command log and Sessions Instrument Panel: identify whether Cypress created, restored, or recreated the session.
  3. Prove setup completes login: assert a protected destination or authenticated UI before setup ends.
  4. Validate the actual authenticated state: use a protected page or API response that distinguishes valid login from stale state.
  5. Rebuild the ID: add every changing input that affects session state; exclude secrets.
  6. Visit the test route: with isolation enabled, call cy.visit() after cy.session().
  7. Inspect data: compare Cypress.session.getSession() with getCurrentSessionData(); ensure setup waits for storage to be applied.
  8. Check cache boundaries: align definitions across specs and remember the cache is per run and machine.
  9. Review migration and domains: replace removed cookie-preservation APIs and account for subdomain cookie scope.

10. Reliability and runtime considerations

A valid cached session can avoid repeating the UI login flow, but validation still has to run to establish that restored state is usable. Choose a small, deterministic authenticated check; a noisy or slow check can make tests flaky or erase the time saved by reuse. There is no universal speedup: login cost and validation cost depend on the application and test environment.

For reliability, keep setup and validation explicit, use stable selectors and endpoints, and make each test visit its own route. If authentication expires during a run, validation should expose that and allow setup to recreate the session. If the app rotates tokens or binds sessions to changing state, ensure the ID and validation model reflect those behaviors.

11. Or skip the browser setup

If the task is capturing a page image for a test artifact or visual review rather than exercising Cypress authentication behavior, ScreenshotNeo provides a website screenshot API and MCP server. It does not replace Cypress session testing, but it can remove browser-launch and capture plumbing for screenshots.

curl -G "https://api.screenshotneo.com/v1/shot" \
  -d access_key=YOUR_API_KEY \
  --data-urlencode url=https://stripe.com \
  -o shot.webp

See the ScreenshotNeo API docs for the request options and formats. Cookie banners, popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000.

Sign up for 1,000 free screenshots a month, with no card required.

12. FAQ

Does cy.session() preserve the current page?

No. It restores cookies and browser storage for the session. Load the route your test needs after the command when test isolation is enabled.

Should a token be part of the session ID?

No. IDs are visible in the reporter. Use non-secret identifiers for the account and state dimensions that determine the session.

Will cacheAcrossSpecs share a login across CI workers?

No. Its cache is limited to one Cypress run on one machine. Each parallel worker establishes its own session.