Every time an automated test suite runs, re-authenticating through UI forms, Two-factor authentication (2FA) prompts, and redirects creates a massive performance bottleneck. You can easily eliminate this hidden “auth tax” using Cypress session handling with cy.session.
By caching your authenticated browser state once, you can skip redundant login steps across your entire suite and dramatically speed up your end-to-end tests. Here is how to set it up step by step.
The Problem: The Compounding Cost of UI Logins
End-to-end tests require an authenticated user. The native, naive approach is to drive the login UI at the start of every single test: type the username, type the password, submit, handle 2FA, and wait for the backend to respond.
While this approach works, it introduces two major issues:
1. The Performance Tax
Consider a suite where each login takes roughly 15 seconds (handling the form, Keycloak redirects, SMS/2FA, and downstream data loading calls).
With many spec files (in our case 100+ specs) using a beforeEach hook to log in, you pay that 15-second penalty hundreds of times per run. This isn’t test logic, it’s pure overhead.
2. The Flakiness Tax
Every UI login introduces a point of failure. A slow 2FA provider, a flaky OAuth redirect, or a brief identity provider hiccup can fail an entire test that has absolutely nothing to do with the actual feature being verified.
So our goal is to log in via the UI as few times as possible, capture the authenticated state, and reuse it everywhere else.
Behind the Scenes: What “Being Logged In” Means
Before fixing the problem, we have to understand what we are preserving. When a user authenticates, the server provides the browser with specific state tokens:
- Cookies: Often an HTTP-only session cookie or auth token.
- localStorage: Where Single Page Apps (SPAs) frequently store access and refresh tokens.
- sessionStorage: Temporary session-scoped flags.
“Being logged in” is simply the existence of these values. If we can capture them once and re-inject them into a fresh browser context, the application instantly considers us authenticated and no UI interaction is required. This is exactly what cy.session automates.
Intro to Sessions
Stabilized in Cypress 12, cy.session is a first-class primitive designed to snapshot and restore browser state.
cy.session(id, setup, options)
The Three Core Pillars
id: A unique key for the session. If theidmatches a cache entry, Cypress reuses it. Any variable that changes the user state (username, role, environment flags) must be part of this ID.setup: A callback containing the original UI login steps. Cypress only runs this code on a cache miss.options: Configuration settings where the true optimisation power lives (validate and cacheAcrossSpecs).
The Execution Lifecycle
[Call cy.session]
│
▼
1. Clear all cookies and storage (Blank canvas)
│
▼
2. Check cache for matching [id]
│
├──► [Hit] ──► Restore state ──► Run validate() ──► Done (Milliseconds)
│
└──► [Miss] ─► Run setup() (UI Login) ──► Snapshot state ──► Cache state
Two Things That Trip Developers Up
1. Sessions Restore State, Not Location
After a session is restored, you are left on a blank page. It does not automatically navigate back to where the session was captured.
Because of this, you must always use a “Session First, Navigate Second“ pattern:
LoginPage.loginSessionRemembered() // Restore or create the session
WorldPage.navigateTo(home) // THEN navigate to a live application route
WorldPage.checkPageURL(home) // Now assertions will pass safely
2. Test Isolation Demands It
Since Cypress 12, test Isolation is enabled by default (testIsolation: true). Every single test starts with a completely wiped browser context. cy.session is the official, sanctioned way to bring authentication back quickly without breaking test independence.
Validate: Trust, But Verify
Cached sessions can expire. Tokens time out, backends invalidate sessions, or time passes between local CI runs. If Cypress blindly injects an expired session, your entire downstream test suite will fail with errors.
The validate option is a guard callback that runs immediately after a session is restored.
- If it passes: The test continues instantly.
- If it fails/throws error: Cypress discards the bad cache and automatically triggers the setup block for a fresh UI login.
The validations should be kept light. Find a signal that the session is alive, such as visiting a protected route and asserting that you aren’t bounced back to the identity provider:
validate() {
cy.visit('/home')
cy.url().should('not.contain', '/auth-server') // Bounced? If yes, validation fails.
}
Cache Across Specs: The True Speed Multiplier
By default, cy.session only caches state within a single spec file. Across a multi-spec suite, your expensive UI login would still execute multiple times.
By setting cacheAcrossSpecs: true, the serialized session survives across the entire global run:
cy.session(id, setup, { cacheAcrossSpecs: true })
Using this setting, the UI login runs exactly once total for the whole test execution suite. Every subsequent spec restores from the global cache in milliseconds.
The golden rule when using cacheAcrossSpecs is that your setup callback must not reference outer closure variables that are omitted from your session ID. If closure variables shift between specs, the cache will silently produce the wrong session state.
How We Implement It
Our application features two distinct authenticated surfaces: the Customer Portal and the internal employee portal (Service Portal). Both leverage independent authentication realms.
1. Customer Portal Implementation
static loginSessionRemembered(userAccount = ..., password = ..., code = ..., isExternal = false) {
cy.session(
[userAccount, password, code, isExternal], // Unique ID array handles user variations
() => {
// Setup Block: Real UI login flow
// Navigate -> Accept Cookies -> Fill Credentials -> Submit -> Handle 2FA -> Hydrate
},
{
cacheAcrossSpecs: true,
validate() {
// Direct cy.visit avoids internal app side-effects during validation
cy.visit(portalBasePath.en + portalRoutes.home, { timeout: 60000 })
cy.url().should('not.contain', Env.getAuthBaseUrl())
},
}
)
}
2. Service Portal Implementation
static loginToServicePortalSessionRemembered(username, password) {
cy.session(
[username, password], // Ensures multi-employee tests use isolated caches
() => {
WorldPage.goToServicePortal()
// Fill credentials + submit
},
{
cacheAcrossSpecs: true,
validate() {
WorldPage.goToServicePortal()
cy.url().should('not.contain', Env.getServicePortalAuthBaseUrl())
},
}
)
}
The Migration Blueprint
Adopting session caching requires reordering how your tests execute steps because of the blank page behaviour.
// Old Anti-pattern
WorldPage.navigateTo(home)
PortalLoginPage.acceptAllCookies()
PortalLoginPage.login(...) // Handled navigation internally via form submission
// New session pattern
PortalLoginPage.loginSessionRemembered(...)
WorldPage.navigateTo(home) // Explicitly command the browser where to go
The Pros and Cons
The Benefits
- Speed: The full login process (Keycloak + 2FA + backend hydration) runs roughly once per test run instead of 100+ times.
- Reliability: Far fewer auth interactions mean far fewer flaky-login failures unrelated to the feature under test.
- Parallel-Friendly: With cacheAcrossSpecs, splitting the suite across CI containers is incredibly cheap. Each container logs in exactly once for its assigned shard rather than once per spec.
Signs It’s Working
- The first spec in your run is slow, but all subsequent specs run instantly.
- Troubleshooting Note: If every single spec remains slow, your cache is not being reused. Double-check that your id keys are completely stable and ensure validate isn’t failing randomly.
Signs of Trouble
- Constant Re-logging: If a whole run continually drops the cache to log back in, a validate check is likely too strict. For example, asserting against an unpopulated environment variable will always fail.
- Post-Restore Failures: If tests crash immediately after a successful session restore, it is usually a violation of the blank-page rule. Add the explicit navigation step immediately following the session call.
Key Takeaways
- Understand the Session: A session is cookies + localStorage + sessionStorage. Capture them once, then reuse them everywhere.
- Smart Execution: cy.session(id, setup, options) runs your expensive
setupcode only on a true cache miss. - Global Gains: cacheAcrossSpecs: true turns “once per spec” into “once per run,” delivering the single biggest performance win for large suites.
- Keep Validation Cheap: validate keeps cross-run or expired sessions honest. Keep it lightweight using a simple navigation + URL check.
- Location Mindset: Session restores leave you on a completely blank page. Always remember: Session first, then navigate.
- Unique Scoping: Everything that uniquely distinguishes a specific user context must live inside the id array.
- Isolate Intent: Never cache the login state for tests that are explicitly written to verify the login process itself.




