Cut Test Runtime by Caching Auth State with Cypress cy.session()
Learn how to use Cypress cy.session() to cache authentication state across tests, eliminate repeated UI logins, and keep your suite fast without hiding login regressions.
21 May 2026, 15:04 UTC

The repeated login tax
If your Cypress suite logs in through the UI before every test, you are paying the same network and rendering cost dozens or hundreds of times. Each login adds seconds to the run and introduces a dependency on the login page markup, making tests brittle when a button label or form field changes.
The useful takeaway: cy.session() lets you run a login setup once per spec file, capture the resulting browser state, and restore it in later tests. The setup can even use an API request instead of the UI, so your tests stop caring about login-page markup entirely.
What cy.session() actually stores
cy.session() is not a wrapper around your login page. It is a browser-state cache. When you give it a name and a setup function, Cypress executes that function, then captures the cookies, localStorage, and sessionStorage present at the end of the setup. On subsequent calls with the same name inside the same spec, Cypress restores that captured context instead of re-running the setup.
Two scoping rules matter. First, sessions are keyed by the string name you provide. Second, they are scoped to the spec file: Cypress clears the cache when the spec finishes, so state does not leak into other files. Cypress also validates whether a cached session is still usable before restoring it; if the session is missing or invalid, the setup function runs again automatically.
Do not confuse cy.session() with cy.origin(). The latter lets you run commands under a different origin within a single test for cross-domain workflows. cy.session() is purely about caching auth state.
Worked example: API login cached once per spec
The fastest setup is an API request that returns the same cookies or tokens your UI login would produce. Below is a spec pattern you can adapt. It assumes Cypress is installed and you run it from the project root with npx cypress open or npx cypress run.
// cypress/e2e/dashboard.cy.js
describe('authenticated dashboard', () => {
beforeEach(() => {
cy.session('dashboardUser', () => {
cy.request({
method: 'POST',
url: 'https://api.example.com/auth/login',
body: {
email: Cypress.env('AUTH_EMAIL'),
password: Cypress.env('AUTH_PASSWORD'),
},
})
})
})
it('displays the main dashboard', () => {
cy.visit('https://app.example.com/dashboard')
cy.contains('Welcome back').should('be.visible')
})
it('lists recent orders', () => {
cy.visit('https://app.example.com/dashboard')
cy.get('[data-cy=orders-list]')
.children()
.should('have.length.greaterThan', 0)
})
})
Placeholders to replace: https://api.example.com/auth/login is your login endpoint, AUTH_EMAIL and AUTH_PASSWORD are environment variables you define in cypress.config.js or your CI secret store. Required permissions: the test runner needs network access to that endpoint, and the credentials must belong to a non-production test account.
Expected check: open the Command Log in the Cypress runner. The cy.request inside the cy.session() setup should appear only once for the whole spec, even though beforeEach runs before both tests. If the setup request appears twice, the session was invalidated between tests.
Risk: if the API changes its response shape or status codes, the setup will fail and every test in the spec will fail with it. That is usually preferable to a silent auth bypass, but it means your login endpoint contract becomes a test dependency.
What you give up
Caching auth state is a trade-off. The biggest risk is that it hides regressions in the actual login flow. If your login page breaks, a suite that never visits it will stay green. Keep at least one dedicated spec that exercises the UI login path end-to-end without cy.session().
A second limitation involves token lifetime. cy.session() restores a snapshot of cookies and storage. If your application relies on short-lived access tokens with refresh logic, a cached session may mask expiration or refresh-token bugs because the test never waits long enough to trigger renewal. To check this, run a separate spec that logs in through the UI, performs an action, waits past the expected token expiry, and then performs another action to confirm the refresh flow still works.
Version differences add another variable. The exact API surface and default behaviors have changed across major Cypress versions; older releases required experimental flags for session and origin support. Before adopting this pattern in an existing project, check the installed version in package.json and review that version's changelog to confirm cy.session() is stable and no experimental flag is required.
A practical way to start
Pick one spec file that logs in through the UI in every test. Replace the repeated UI login with a cy.session() call in beforeEach, using an API request in the setup function. Run the spec and confirm in the Command Log that the setup ran once. Then run your full suite and compare total runtime against the previous run under similar network conditions. If the setup request appears once and the suite finishes faster, the cache is working as intended.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.