My restored Cypress session was lying to me A developer discovered that their Cypress test suite's session validation was ineffective due to two stacked bugs: a relative URL that never reached the identity provider and a single-page app returning a 200 OK for any path. This caused tests to fail sporadically, misleadingly labeled as flaky, and cost days of debugging. The developer recommends verifying that validation requests hit the correct server and receive the expected content type. Author's Note / Disclosure: 100% human-authored content based on real production engineering work. No AI was involved in writing the article, technical analysis, or code. cy.session is the single biggest speed win available to an authenticated Cypress suite. You log in once, Cypress snapshots cookies, localStorage and sessionStorage , and every later spec restores that snapshot instead of walking through an identity provider. The safety net is validate . Cypress runs it after restoring a cached session; if it throws, fails an assertion, or yields false , Cypress throws the snapshot away and runs setup again. That is the whole contract: a bad session gets detected and replaced. Mine could not fail. For weeks. And it cost me days of chasing "flaky" specs that were nothing of the kind. js Cypress.Commands.add 'login', user: User = { cy.session user.username, = { cy.visit '/' cy.origin idpOrigin, { args: user }, { username, password } = { cy.get ' username' .type username cy.get ' password' .type password, { log: false } cy.get 'button type="submit" ' .click } cy.get ' app-shell' .should 'be.visible' }, { cacheAcrossSpecs: true, validate { cy.request '/connect/userinfo' .its 'status' .should 'eq', 200 }, }, } Reasonable, right? /connect/userinfo is the OIDC user info endpoint. If the session is dead it should 401, validate fails, and we log in again. Two independent bugs stack up here, and either one alone is enough to make the check worthless. The URL is relative. cy.request '/connect/userinfo' resolves against baseUrl , which is the application, not the identity provider. So the request never touches the IdP. The application is a single-page app. Its host serves index.html for any path it does not recognise, because that is what history-API routing requires. A request for /connect/userinfo gets back the SPA shell with 200 OK and content-type: text/html . validate asserted on a status code. It got a 200 . Every single time. Including for a session whose refresh token expired three hours ago. You can confirm this in one command, without Cypress: curl -is https://app.example.com/connect/userinfo | head -5 If you see 200 and text/html , your validate is decorative. This is the part that burns time. The session restores, validate passes, and then the first real command of your test triggers the app's own auth check, which redirects to the identity provider. Your spec fails like this: AssertionError: Timed out retrying after 10000ms: Expected to find element: dashboard-grid , but never found it. Nothing points at authentication. The URL in the screenshot is the IdP login page, but you are looking at a grid selector, so you go and inspect the grid. Across a large suite the pattern shows up as an unrelated scatter of failures in whichever specs happened to run after the token expired — the exact fingerprint people label "flaky" and hand a retry. The tell: the failure moves around between runs, but always lands on the first assertion after a restored session. I tried the two obvious ones first. Point at the absolute IdP URL. cy.request 'https://idp.example.com/connect/userinfo' now reaches the right server, but cy.request sends the browser's cookie jar and nothing else. If your SPA authenticates with a bearer token — and if you are using oidc-client-ts , MSAL , angular-auth-oidc-client or friends, you are — then the token lives in web storage and is attached by an HTTP interceptor inside the app. cy.request has no interceptor. You get a 401 for a session that is perfectly alive, setup re-runs on every spec, and you have deleted the entire benefit of cy.session . Call a real application API instead. Same problem, same reason. You are testing whether cookies alone can authenticate, which is not how the app works. Both failures come from the same mistake as the original bug, just inverted: the check is exercising a credential path the application does not use. The application decides it is logged in by reading a token out of storage. So that is what validate should assert on. interface StoredUser { access token: string expires at?: number } function readStoredUser win: Window : StoredUser | null { const key = Object.keys win.sessionStorage .find k = k.startsWith 'oidc.user:' if key return null const raw = win.sessionStorage.getItem key return raw ? JSON.parse raw as StoredUser : null } function expiryMs user: StoredUser : number | null { if user.expires at return user.expires at 1000 const , payload = user.access token.split '.' if payload return null const json = JSON.parse atob payload.replace /-/g, '+' .replace / /g, '/' , as { exp?: number } return json.exp ? json.exp 1000 : null } And the validate itself: js validate { cy.visit '/' cy.window { log: false } .then win = { const user = readStoredUser win expect user, 'stored auth record' .to.not.be.null const expires = expiryMs user as StoredUser expect expires, 'token expiry claim' .to.be.a 'number' expect expires as number, 'token still valid with margin' .to.be.greaterThan Date.now + 30 000 } } Three details in there matter more than the rest. The cy.visit '/' is not optional. validate runs before your test's own cy.visit . Until a page on the app origin is loaded, the application under test is a blank frame, and cy.window hands you storage that is empty regardless of session health. Skip the visit and you have built the opposite bug: a validate that can never pass , so setup runs every spec. Read the storage from a page on the origin that owns it. The 30-second margin. A token with four seconds left passes a naive Date.now check and then expires in the middle of your test. Require enough runway to finish the spec. Tune the number to your slowest test, not your fastest. The storage key prefix is library-specific. oidc.user: