QA How-To
How to Use Cypress cy.session (2026)
Learn cypress cy.session with validated login caching, safe session IDs, test isolation, cross-spec reuse, debugging, and TypeScript custom commands for 2026.
20 min read | 2,903 words
TL;DR
Wrap login setup in cy.session(id, setup, { validate }). Cypress creates the state once, restores cookies and web storage for later calls with the same ID, validates it, and reruns setup if a restored session is invalid. Call cy.visit after the helper when test isolation leaves the browser on a blank page.
Key Takeaways
- cy.session caches and restores cookies, localStorage, and sessionStorage for a unique session ID.
- Place cy.session inside a reusable login helper or custom command, then visit the page under test after login.
- Use a validate callback that proves the restored identity can still access a protected endpoint.
- Build the session ID from every non-sensitive input that changes the resulting authenticated state.
- Set cacheAcrossSpecs only for sessions reused in multiple specs within the same run and machine.
- Keep test isolation enabled unless the suite has a documented reason and strong state controls.
- Use Cypress.session clearing helpers for diagnosis, not as a substitute for correct IDs and validation.
Cypress cy.session caches and restores cookies, localStorage, and sessionStorage so repeated tests do not rebuild the same authenticated state. A reliable implementation supplies a unique ID, performs a complete login in setup, validates the resulting identity, and visits the page under test after the session is ready.
The speed improvement is useful, but correctness comes first. A weak ID can restore the wrong user, and a missing validation callback can reuse an expired session. This guide explains the lifecycle, test isolation behavior, cross-spec scope, custom-command design, security concerns, and debugging workflow an SDET should understand in 2026.
TL;DR
function login(email: string) {
cy.session(
['api-login', email],
() => {
cy.request({
method: 'POST',
url: '/api/auth/session',
body: { email, password: 'secret-from-config' },
log: false,
}).its('status').should('eq', 204)
},
{
validate() {
cy.request('/api/me').its('status').should('eq', 200)
},
},
)
}
it('opens the account', () => {
login('member@example.test')
cy.visit('/account')
cy.contains('h1', 'Account').should('be.visible')
})
| Part | Responsibility | Failure meaning |
|---|---|---|
id |
Uniquely identifies the resulting state | Collision can restore the wrong state |
setup |
Establishes complete auth state | First creation fails the test |
validate |
Proves created or restored state is usable | Restored state is recreated, new invalid state fails |
cacheAcrossSpecs |
Extends reuse within one run and machine | Inconsistent definitions cause an error |
1. What Cypress cy.session Stores and Restores
The command signature is cy.session(id, setup) or cy.session(id, setup, options). The ID may be a string, array, or object. Cypress deterministically stringifies arrays and objects to create the cache key. setup is a function, and the supported options are validate and cacheAcrossSpecs. The command yields null.
On the first call for an ID, Cypress clears cookies and web storage associated with the browser context, runs setup, optionally runs validate, and saves the resulting cookies, local storage, and session storage. On a later call with the same ID, Cypress restores the saved data and runs validation if provided. If restored-state validation fails, Cypress reruns setup and validates the rebuilt state.
This behavior makes cy.session() a state cache, not a test shortcut that skips authentication correctness forever. Setup must reach a stable authenticated point before it finishes. For a UI login, assert the post-login URL or a durable logged-in element. For an API login, assert the response and then validate a protected resource.
The cache does not preserve the DOM, in-memory React state, open network connections, or an arbitrary JavaScript closure. It preserves browser cookies and the two Web Storage mechanisms. The test still needs to visit or reload the application and let it initialize from that state.
cy.session() became generally available in modern Cypress releases long ago, so current suites should not add obsolete experimental session flags. If you encounter one during a migration, remove it only after checking the repository's installed Cypress version and migration notes.
2. Set Up Cypress cy.session With Test Isolation
Test isolation and session caching solve related but different problems. Test isolation prevents one test's browser context from contaminating another. Session caching reconstructs an explicitly named context. Keeping testIsolation: true, the default end-to-end behavior in a normal configuration, makes a test's starting state predictable.
// cypress.config.ts
import { defineConfig } from 'cypress'
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
testIsolation: true,
},
})
With isolation enabled, Cypress clears the page as part of the session lifecycle. After the login helper finishes, call cy.visit() for the page the test actually exercises. Do not put a visit to every possible destination inside the login command. Login should establish identity, while the test should declare navigation.
beforeEach(() => {
cy.login('member@example.test', 'member')
cy.visit('/dashboard')
})
Disabling isolation changes page-clearing behavior, but cookies and browser storage are still cleared before session setup. A non-isolated suite can appear faster while creating order dependencies and surprising behavior with .only(). Use it only for a deliberately designed suite whose state boundaries are documented and reviewed. Session caching is not a reason by itself to disable isolation.
Run a small baseline before changing login. Record how many times the real identity provider is called and which tests truly need a distinct state. Optimize the repeated work, not the security checks that make failures meaningful. The Cypress test isolation guide provides a wider treatment of browser-state boundaries.
3. Put Login in a Typed Custom Command
Cypress recommends using cy.session() inside a reusable login command or helper. A single definition prevents copied session logic from diverging across specs. It also gives the team one place to protect secrets, define IDs, and strengthen validation.
// cypress/support/commands.ts
type UserRole = 'member' | 'admin'
Cypress.Commands.add('login', (email: string, role: UserRole) => {
return cy.session(
['api-login', email, role],
() => {
cy.request({
method: 'POST',
url: '/api/auth/session',
body: {
email,
role,
password: 'secret-from-config',
},
log: false,
}).then((response) => {
expect(response.status, 'login response').to.eq(204)
})
},
{
validate() {
cy.request<{ email: string; role: UserRole }>('/api/me')
.its('body')
.should('include', { email, role })
},
},
)
})
// cypress/support/index.d.ts
declare global {
namespace Cypress {
interface Chainable {
login(email: string, role: 'member' | 'admin'): Chainable<null>
}
}
}
export {}
Replace the tutorial password with a value loaded through the project's protected Cypress configuration. log: false reduces sensitive command output, but do not assume it cleans screenshots, server logs, thrown errors, or custom logging.
A plain function is equally valid when the project avoids custom commands. The important properties are a returned Cypress chain, one authoritative definition, domain-focused parameters, and complete validation. Do not create loginMember, loginAdmin, and loginAuditor as copied blocks if a typed role parameter represents the real variation.
4. Design a Unique and Safe Session ID
The ID is the identity of the cached browser state. If two setup variations use the same ID, Cypress can restore the first state when the second was intended. Include every non-sensitive input that can change cookies or web storage: user identity, role, tenant, authentication strategy, locale when stored, and feature profile when it changes boot state.
cy.session(
['api-login', { email, role, tenantId, featureProfile }],
setup,
{ validate },
)
Do not include passwords, access tokens, session cookies, client secrets, or one-time codes. Session IDs appear in the runner and diagnostic output. A random ID is also usually wrong because it defeats reuse. The goal is a deterministic description of the intended state, not a secret or a unique value on every call.
| Input | Include in ID? | Reason |
|---|---|---|
| User email or stable test-user key | Yes | Distinguishes identity |
| Role or permission set | Yes | Changes authorization state |
| Tenant | Yes | Changes data boundary |
| Login method | Yes | Cookie and token shape can differ |
| Password | No | Sensitive and not the intended state |
| Access token | No | Sensitive and frequently rotated |
| Timestamp or random UUID | Usually no | Prevents useful cache hits |
If a test mutates the cached user's preferences or permissions, the original session snapshot is not automatically rewritten. Either create a new state with a different ID, restore the intended state through setup, or avoid using a mutable shared identity. The cache key should describe the state that setup produces, and validation should detect when that description no longer matches reality.
5. Write Validation That Proves the Session Works
Validation runs immediately after a newly created session and after a cached session is restored. A thrown exception, failing Cypress command, rejected Promise, resolved false, or last Cypress command yielding false marks validation as failed. Failure after a restore causes setup to run again. Failure immediately after setup fails the test because the newly built state is invalid.
A protected identity endpoint is often the best validation target. It is faster and more precise than loading a large dashboard, and it can verify both authentication and authorization.
validate() {
cy.request<{ id: string; email: string; permissions: string[] }>({
url: '/api/me',
failOnStatusCode: false,
}).then(({ status, body }) => {
expect(status).to.eq(200)
expect(body.email).to.eq(email)
expect(body.permissions).to.include(requiredPermission)
})
}
Using failOnStatusCode: false here lets the assertion describe a 401 or 403 clearly. It is not mandatory. A normal request that fails on status also invalidates a restored session. What matters is that validation checks the expected identity, not merely that some cookie exists. An expired cookie can exist, and a session for the wrong tenant can still return 200.
Do not make validation destructive. It runs on every restoration, so creating orders or changing preferences inside it introduces hidden side effects. Prefer an idempotent read. Keep it fast because validation cost is paid repeatedly even when setup is skipped.
If validation requires a page visit, use a protected lightweight route and assert the stable authenticated outcome. API validation is generally easier to diagnose and avoids coupling the login cache to unrelated UI rendering.
6. Use cacheAcrossSpecs Deliberately
By default, a session is cached for the current spec. Set cacheAcrossSpecs: true to make it available to other specs in the same Cypress run on the same machine. This option is useful when many specs share a stable test identity and login setup is expensive.
function loginGlobal(email: string) {
return cy.session(
['global-api-login', email],
() => {
cy.request({
method: 'POST',
url: '/api/auth/session',
body: { email, password: 'secret-from-config' },
log: false,
}).its('status').should('eq', 204)
},
{
validate() {
cy.request('/api/me').its('status').should('eq', 200)
},
cacheAcrossSpecs: true,
},
)
}
Every spec that uses a global cached session must call cy.session() with the same ID, setup, validation, and cacheAcrossSpecs value. Define the logic once in support code. Copying it into specs invites a mismatch that Cypress correctly rejects.
The global cache lasts only for that run on that machine. It is held in memory, not written to disk for future runs. When CI splits specs across multiple machines, each machine builds its own session at least once. Do not claim that one login will serve every parallel worker or the next pipeline.
Cross-spec caching can amplify shared-account risk. If specs change the user's server-side permissions, profile, cart, or organization, restoring browser storage does not undo those backend mutations. Prefer immutable test identities, worker-specific tenants, or backend reset endpoints. Read Cypress parallel testing practices before sharing cached identities across a large CI matrix.
7. Understand Cookies, Web Storage, and Origins
The session snapshot contains cookies, localStorage, and sessionStorage for the relevant origins. It does not contain IndexedDB, the DOM, service worker runtime state, or arbitrary application memory. If authentication depends on an unsupported store, redesign the setup or initialize that state separately and explicitly.
For cookie authentication, a direct cy.request() login is usually concise because response cookies synchronize with the browser. For token authentication, put the token exactly where the application expects it. Visiting the application origin during setup makes local-storage ownership explicit.
cy.session(['token-login', email], () => {
cy.request<{ accessToken: string }>('POST', '/api/auth/token', {
email,
password: 'secret-from-config',
}).then(({ body }) => {
cy.visit('/', {
onBeforeLoad(win) {
win.localStorage.setItem('access_token', body.accessToken)
},
})
})
cy.location('pathname').should('not.eq', '/login')
}, {
validate() {
cy.request('/api/me').its('status').should('eq', 200)
},
})
Cross-origin authentication requires extra care. Visit the required domain explicitly as part of the login design, use cy.origin() where the browser workflow crosses origins and Cypress requires it, and keep the session ID tied to the authentication method. Never assume storage under one origin is readable by another.
Single sign-on flows can be unstable when they rely on external identity-provider UI, CAPTCHA, or one-time codes. Prefer an approved programmatic test authentication path that preserves the same application session semantics. Coordinate it with security and keep it inaccessible in production.
8. Visit the Page After Restoring a Session
A common failure looks simple: login passes, then every cy.get() fails because the page is blank. With test isolation enabled, cy.session() clears the page. Restoring cookies and storage does not navigate to the dashboard. The test must visit its destination.
describe('billing', () => {
beforeEach(() => {
cy.login('billing@example.test', 'member')
cy.visit('/billing')
})
it('shows the current plan', () => {
cy.contains('[data-testid=current-plan]', 'Pro').should('be.visible')
})
})
Keep the destination outside the login helper so different tests can reuse the same session on /billing, /projects, and /settings. This also makes each test's precondition visible.
Do not add an arbitrary cy.wait() after cy.session(). The command finishes only after creation or restoration and validation completes. If the visited application has a documented bootstrap request, intercept that application request before the visit and wait for the alias. If it exposes a stable ready element, assert that element. Time-based waiting hides the actual readiness contract.
Another anti-pattern is calling cy.visit() inside validate and assuming that page remains ready for the test. Validation should prove session health, not navigate to an accidental destination. The session lifecycle may clear the page. Always let the test own the final visit.
9. Model Roles, Tenants, and Expiration
A mature suite rarely has one generic logged-in user. It has members, administrators, suspended users, multiple tenants, feature entitlements, and expired credentials. Treat each resulting browser state as a distinct session identity.
type LoginContext = {
email: string
role: 'member' | 'admin'
tenantId: string
}
function loginAs(context: LoginContext) {
const { email, role, tenantId } = context
return cy.session(['login', email, role, tenantId], () => {
cy.request('POST', '/test-support/login', context)
.its('status')
.should('eq', 204)
}, {
validate() {
cy.request<{ email: string; role: string; tenantId: string }>('/api/me')
.its('body')
.should('include', { email, role, tenantId })
},
})
}
Do not cache an intentionally expired state and validate it as though it should be healthy. Expiration tests often need explicit cookie or token construction and a direct assertion that the application returns to login. Keep those scenarios separate from the reusable happy-path helper.
When permissions change server-side, the stored browser state may remain syntactically valid while authorization changes. Validation that includes required role or tenant catches this. For especially sensitive tests, create a dedicated identity or tenant so another parallel test cannot revoke or upgrade it.
This model also makes access-control coverage readable. A test title can say what an admin may do, while loginAs() declares the context and cy.visit() declares the resource.
10. Debug and Clear Cached Sessions
The Cypress runner displays session creation, restoration, and recreation in its session instrument panel and Command Log. Expand the group to see commands executed during setup or validation. Inspect the session ID first, then determine whether setup ran, validation failed, or an unexpected cached state was restored.
During diagnosis, clear saved sessions and rerun. Current Cypress provides session helpers such as:
// Clear saved spec and global sessions from the backend cache.
Cypress.session.clearAllSavedSessions()
// Clear cookie and storage data for the current browser context.
Cypress.session.clearCurrentSessionData()
Use these methods intentionally in debugging or a controlled support workflow. Calling clearAllSavedSessions() before every test eliminates reuse and hides ID collisions rather than fixing them. The runner also offers a Clear All Sessions control in open mode.
When a test reports 401 after restoration, check whether setup asserted completion before the state was captured, whether the session cookie's domain and security attributes match the app, whether a token has expired, and whether validation proves the correct identity. When the wrong role appears, inspect the ID inputs and shared server-side mutations. When commands fail on a blank page, add the destination visit after login.
For intermittent CI-only behavior, compare worker allocation. A cross-spec session is not shared across machines, so each worker needs valid credentials and setup data. Preserve backend correlation IDs and avoid printing secrets while collecting enough evidence to distinguish identity-provider failure from an application assertion.
11. Cypress cy.session Architecture Checklist
A code review should be able to answer these questions without executing the suite.
- Is
cy.session()defined once in a helper or custom command? - Does the ID include user, role, tenant, and login strategy where relevant?
- Does the ID exclude passwords, tokens, cookies, and random values?
- Does setup assert a durable post-login state before ending?
- Does validation check a protected, idempotent resource and expected identity?
- Does the test call
cy.visit()after the session helper? - Is
cacheAcrossSpecsjustified, and is the helper identical for every caller? - Can parallel workers mutate the same server-side account state?
- Are secrets omitted from logs, titles, aliases, and artifacts?
- Do expiration and unauthorized tests bypass the healthy-session cache appropriately?
- Does debugging clear state only when needed rather than on every test?
A strong session implementation is intentionally boring. It has a deterministic key, one complete setup, a fast validation, and a clear navigation step. If a helper needs many waits, page-specific branches, or manual cache clearing, the login boundary is probably not modeled cleanly.
Interview Questions and Answers
Q: What does cy.session() cache?
It caches and restores cookies, local storage, and session storage. It does not preserve the DOM, application memory, or every browser database. The test still visits the application and lets it bootstrap from restored state.
Q: When does setup run?
It runs when no cached session exists for the ID or when restored-state validation fails. On initial creation, validation failure fails the test because setup produced an invalid state. On restoration, validation failure causes Cypress to rebuild the session.
Q: How do you choose the ID?
I include all non-sensitive inputs that affect the resulting state, such as user key, role, tenant, and login method. I exclude passwords and tokens because IDs can appear in diagnostics. I also avoid random values because they prevent cache reuse.
Q: Why use validate?
A cached cookie or token may be expired, revoked, or attached to the wrong identity. Validation proves the state still works and matches the expected user or permission. A fast protected GET is usually the best validation action.
Q: What does cacheAcrossSpecs do?
It allows the same session definition to be reused across specs in one Cypress run on one machine. It does not persist to another run or synchronize parallel machines. Every caller must use the same ID, setup, validation, and option value.
Q: Why is the page blank after cy.session()?
With test isolation enabled, the page is cleared during the session lifecycle. Restoring storage does not navigate. Call cy.visit() after the login helper for the route the test will exercise.
Q: Should test isolation be disabled for speed?
Usually no. Isolation prevents order-dependent browser state, while cy.session() restores only explicitly named state. Disabling isolation needs a separate architectural justification and careful control of cross-test effects.
Q: How would you debug the wrong user being restored?
I inspect the session ID and all setup inputs first, then check shared backend mutations and validation depth. I clear saved sessions once to reproduce creation, but I fix the collision or validation rather than clearing on every test.
Common Mistakes
- Using the email alone when role, tenant, or login method also changes the state.
- Putting a password, token, or one-time code in the session ID.
- Omitting validation or checking only that a cookie exists.
- Letting setup finish before the login is durably complete.
- Expecting the destination page to remain loaded after session restoration.
- Setting
cacheAcrossSpecs: truein copied, inconsistent helpers. - Assuming a global session is shared across parallel CI machines or future runs.
- Mutating a shared user's server-side settings in multiple specs.
- Clearing all saved sessions before every test and eliminating the cache benefit.
- Disabling test isolation only to avoid adding an explicit
cy.visit().
Conclusion
Cypress cy.session is a controlled cache for browser authentication state, not a blanket login bypass. Give each state a deterministic, non-sensitive ID, complete setup before capture, validate the correct identity through a protected read, and navigate explicitly after restoration.
Start by wrapping one repeated login in a shared helper with validation. Run it across roles, spec reruns, and parallel CI workers, then add cacheAcrossSpecs only when cross-spec reuse offers a real benefit. That sequence delivers speed while preserving the isolation and evidence a trustworthy test suite needs.
Interview Questions and Answers
Explain the cy.session lifecycle.
Cypress clears the relevant session data, runs setup for a new ID, validates if configured, and caches cookies plus web storage. Later calls restore that state and validate it. If restored validation fails, Cypress reruns setup, while invalid state immediately after setup fails the test.
What data does cy.session preserve?
It preserves cookies, localStorage, and sessionStorage. It does not preserve DOM state, JavaScript memory, or every browser persistence mechanism. The application must be visited and initialized after restoration.
How do you create a good session ID?
I build a deterministic array or object from every non-sensitive input that changes the desired state, including user, role, tenant, and auth method. I do not use credentials or random values. This prevents collisions without exposing secrets or defeating reuse.
What is a strong validation callback?
It performs a fast, idempotent call to a protected identity endpoint and checks the expected user and authorization context. Checking only cookie existence is weak because an expired or wrong-user cookie can still exist.
How does cacheAcrossSpecs behave in CI?
It reuses the session across specs only within the same run and machine. Parallel machines have separate memory and each performs setup. Shared server-side accounts can still race, so I use stable or worker-specific identities.
How does test isolation affect cy.session?
With isolation enabled, Cypress clears the page during the session lifecycle, and the test must visit its target route afterward. Isolation remains valuable because it prevents unnamed state from leaking between tests. Session caching restores only the state explicitly associated with an ID.
How do you debug a cy.session 401?
I inspect whether setup reached a stable authenticated state, whether cookie domain or token storage matches the application, and whether validation proves the correct identity. I clear the cache once to compare creation with restoration, then fix the ID, setup, or validation rather than clearing continuously.
Should login UI be used inside cy.session?
It can be, and setup should assert the durable post-login result. A programmatic login is usually faster and less coupled when an approved test endpoint exists. Either method must create the same browser state the application expects and be validated.
Frequently Asked Questions
What is Cypress cy.session used for?
It caches and restores cookies, localStorage, and sessionStorage for a named browser state. Its most common use is avoiding repeated login setup while keeping tests isolated.
Does cy.session cache the current page?
No. It caches browser cookies and web storage, not the DOM or rendered application state. With test isolation enabled, call cy.visit after restoring the session.
Is the validate option required?
It is optional in the API, but strongly recommended for authentication. Validation detects expired, revoked, incomplete, or wrong-identity state and lets Cypress recreate an invalid restored session.
Can cy.session share login across spec files?
Yes, set cacheAcrossSpecs to true and use exactly the same session definition. Reuse is limited to one Cypress run on one machine, so parallel machines build separate sessions.
What should a cy.session ID contain?
Include non-sensitive values that change the resulting state, such as user key, role, tenant, and login method. Exclude passwords, tokens, cookies, and random values.
Why does login setup run again?
Either no matching cache exists or validation marked a restored session invalid. Check expiration, the validation request, the ID, and whether setup fully completed before capture.
Can I clear cy.session data manually?
Yes. Cypress.session provides helpers for clearing saved sessions and current browser session data, and open mode has a Clear All Sessions control. Use clearing for diagnosis rather than before every test.