Est.

Cookie and Authentication State Persistence Across Sessions

Building secure session persistence requires balancing seven architectural layers.

Features Editor · · 10 min read
Cover illustration for “Cookie and Authentication State Persistence Across Sessions”
Browser Sessions · September 22, 2026 · 10 min read · 2,181 words

"Stay logged in" is a checkbox in most product specs and an architectural decision in practice, one with at least seven interlocking layers: token format, storage location, cookie attributes, rotation strategy, revocation, edge enforcement, and now, agent behavior. Get any single layer wrong and the rest don't matter much. The cookies-versus-JWT debate that appears in engineering forums treats this like a binary choice, but the more useful question is where each piece of the session lives and which attack surface a team is choosing to defend by putting it there.

What cookies and JWTs each do, and why modern apps use both

A session cookie, at its core, is nothing more than an opaque random string sent back and forth in a Cookie header. It carries no meaning on its own. The actual session data, who the user is, what they're allowed to do, when the session started, lives server-side in something like Redis, Postgres, or memcached, keyed by that random string. The cookie is a claim ticket; the actual session data is the luggage.

A JWT works differently. It's a signed, self-contained payload, a header, a set of claims, and a signature, stitched together as three base64url segments joined by dots. Because it's signed, a server can verify it locally without touching a database. That's the entire appeal: verification becomes a math problem instead of a network round trip.

So why do most modern applications use both instead of picking one? Because they solve different problems. A short-lived JWT works well as an access token: fast to verify, cheap to scale, and expires quickly enough that a stolen one has a short shelf life. But something has to renew that access token when it expires, and that something needs to be durable and hard to steal. That's the refresh token, and as of 2026 the accepted pattern puts it inside an HttpOnly, Secure, SameSite cookie, paired with refresh-token rotation and reuse detection. The access token lives in memory. The refresh token lives in a cookie the browser guards. Two tokens, two lifespans, two different jobs.

Diagram: Two Tokens, Two Jobs, Two Lifespans. Visualizes: Visualize the dual-token architecture where a short-lived JWT access token (lives in memory, fast to verify, no database round trip) pairs with a longer-lived refresh token (lives in an…

A cookie with no attributes set behaves like it's from the early, unhardened era of the web: sent everywhere, readable by any script, transmitted over plaintext without complaint. Every meaningful security decision about a cookie happens in its attributes, not in the value it carries.

HttpOnly blocks document.cookie from ever seeing the value in JavaScript. Every authentication cookie should have it set, full stop. Secure ensures the cookie only travels over HTTPS, and in production, there's no excuse for leaving it off.

SameSite is where things get messy, mostly because three options that sound similar behave very differently. Strict means the cookie never rides along on a cross-site request, including top-level navigation. Click a link from a Slack DM into an app protected by an auth cookie with the strictest same-site setting, and the request arrives unauthenticated even if the user is logged in on that same browser. That's correct behavior for a refresh-token cookie scoped to something like /auth, but it would break normal browsing if applied everywhere. Lax sends the cookie on top-level GET navigations but withholds it on cross-site POST requests, fetch calls, and iframes. For most general-purpose session cookies, Lax is the sane default. None sends the cookie on every cross-site request regardless of context, which is what OAuth flows and third-party embeds need, but it has to be paired with Secure or every major browser drops it silently, no warning, no error, just a cookie that never arrives.

Browser defaults compound the confusion. Chromium-based browsers have defaulted unset cookies to Lax since Chrome 80. Firefox tried to follow the same path but reverted the change because of breakage, and currently defaults to None. Safari also defaults to None but leans on Intelligent Tracking Prevention to compensate. Relying on any browser's default here is a bet with no clear payout: set SameSite explicitly, every time, on every cookie that matters.

Where token storage location determines the threat model

Where a token lives is a decision about which category of attack the application is willing to defend against, and which one it's accepting exposure to. It's a decision about which category of attack the application is willing to defend against, and which one it's accepting exposure to.

localStorage is readable by any JavaScript running on the page. Any cross-site scripting vulnerability, however minor, becomes a direct path to token theft. There's no browser mechanism standing between an injected script and a token sitting in localStorage. Once XSS exists, the token is gone.

An HttpOnly cookie removes that exposure by hiding the token from JavaScript. But it doesn't eliminate risk, it trades one risk for another: cross-site request forgery becomes the surface to defend, and that's what the SameSite attributes from the previous section exist to mitigate. Neither approach is free. One trades XSS exposure for CSRF exposure, and the SameSite configuration is what determines how much of that CSRF exposure actually remains.

Keeping a token in memory, as a plain JavaScript variable, sidesteps both problems in a narrower way. It survives only as long as the current tab or session stays open, and it disappears the moment the page refreshes or the tab closes. That's a real cost in convenience, but it's also why in-memory storage fits short-lived access tokens so well: there's nothing persistent for an attacker to steal after the fact, because the token was never designed to persist.

Rotation strategy: how refresh tokens stay fresh without opening replay windows

A refresh token that never changes is, functionally, a long-lived credential wearing a short-lived costume. If it's stolen once, an attacker holds persistent access until someone notices and revokes it manually, which could be hours or weeks later.

Refresh token rotation closes that gap by design: every time the refresh token gets used, the server issues a new one and immediately invalidates the old one, rewriting the HttpOnly cookie with the fresh value. The old token becomes worthless the instant it's spent.

That rotation also enables something more useful than expiration: reuse detection. If a server ever sees an already-invalidated refresh token presented again, that's a signal that something is wrong. Someone has a copy of a token that should no longer exist. A token was likely stolen at some point. A robust response goes beyond rejecting that one request to invalidating the entire token family tied to that lineage, forcing full re-authentication. The invalidation of the entire token family tied to that lineage is the actual detection mechanism at work.

For asymmetric JWTs, there's a parallel rotation story on the signing side. The key pair used to sign tokens should rotate on a schedule, and a well-known JWKS endpoint gives clients a consistent location to retrieve current public keys as the signing pair rotates.

Diagram: Refresh Token Rotation and Reuse Detection. Visualizes: Show the lifecycle of refresh token rotation as a stepped sequence: (1) client presents refresh token, (2) server issues new refresh token and invalidates the old one, (3) HttpOnly…

Revocation: why the stateless JWT promise breaks under real operational requirements

The entire pitch of a JWT is that a server can verify one locally, no database lookup required. That same property is what makes revocation hard, because there's no central record to check against, no row to delete, no flag to flip. The token is valid until it expires, full stop, regardless of what happens to the account behind it.

Opaque access tokens solve this cleanly through introspection: revoking access is one database update, and every verification already involves a lookup anyway, so there's no added cost. The tradeoff was baked in from the start; that's the entire premise.

JWTs don't offer that same escape hatch. Getting real revocation out of a token-based system with no server-side lookup means reintroducing the server-side state the format was designed to avoid, through a blacklist of revoked token IDs, deliberately short expiry windows that force frequent reissuance, or tracking whole token families so a compromised one can be shut down along with its lineage. None of these are flaws in the JWT format. They're the cost of the tradeoff the format made in the first place, and any team choosing JWTs for access tokens should walk in already knowing which of these three they're going to build.

How edge and serverless runtimes complicate session state

Most serverless functions get written as though they have no memory between invocations, and that's mostly true, but developers routinely forget it applies to session state too. Store a token or session flag in a plain process variable, and it survives exactly until the next invocation, or the next cold start, whichever comes first. Then it's gone, silently, with no error to point at.

The Agents SDK's MemorySession illustrates this well. It's process-local storage, which works fine right up until the process exits, and on edge runtimes, processes exit often. Teams that treat that in-memory state as durable are, without knowing it, betting that a process will stay alive indefinitely. Teams that treat that in-memory state as durable are, without knowing it, betting that a process will stay alive indefinitely, and it won't. Sessions drop, and nothing in the logs necessarily flags it as an auth failure, because from the runtime's perspective, nothing failed. The state simply wasn't there to begin with.

Running Better Auth on Workers with D1 (SQLite) produced 33-second hangs and session dropouts under load. The root cause traced back to SQLite's write-ahead log lock contention: the second writer to hit the database blocks, waiting for the first writer's lock to clear, and that wait stretched past 30 seconds in practice. That's what happens when a storage engine built around a single writer gets asked to handle concurrent session writes at the edge.

One approach to that kind of load uses a single consistent writer for session state and agent conversation history, rather than a shared database fighting over the same lock.

AI agent sessions: why parallel browser contexts break standard auth assumptions

Tell an agent to process a few hundred tasks and it fans out into a few hundred parallel browser sessions, or close to it. Most of them stall at the same login screen, because auth state was never pre-shared across those sessions, each one starts cold. Worse, the identity provider often reads that sudden burst of near-simultaneous logins as suspicious activity, not automation, and responds with MFA prompts or CAPTCHAs that no headless agent can solve on its own.

The fix starts with recognizing what "signed in" actually consists of. "Signed in" consists of more than one token. It's cookies, localStorage, sessionStorage, and whatever tokens the app issues, bundled together, and an agent that needs to reuse a signed-in state has to carry all of it, including the pieces that look less important.

The browser-use pattern for this uses BrowserConfig to manage persistence settings across cookies, user agent, and viewport, letting an agent resume a session instead of re-authenticating from scratch. But the cookie files themselves are stored as plaintext on disk, so they belong in .gitignore and nowhere near a commit history. Anyone who gets hold of that file can impersonate the session it belongs to, no password required, no MFA prompt to clear.

That points to a deeper identity question for agent workflows generally. A Worker instance or a container process is not a durable identity, it can be killed and replaced without warning. A durable session identifier tied to the application record is the thing that should persist, letting the platform swap out the underlying executor whenever it needs to, without breaking continuity for the job itself.

Zero Trust session verification: continuous re-evaluation rather than one-time login

Zero Trust starts by rejecting a premise most session architecture quietly assumes: that login is the moment trust gets established and everything after that is just riding on it. NIST 800-207 uses the phrase "continuously verified," and that's the operative idea, trust gets re-checked on every request, not issued once at sign-in and left alone until the next one.

What does that look like in practice? A device that passes its posture check at 9 a.m. and fails it at 9:47 because an EDR agent got disabled shouldn't keep its access until the next login prompt, it should lose it immediately, mid-session, the moment the check fails. Disk encryption status, EDR presence, OS patch level, jailbreak or root detection: these get evaluated continuously, as session attributes, not as one-time gates checked only at the front door.

Identity continuity isn't guaranteed just because a user is technically "still logged in." If someone authenticates through an identity provider and later re-authenticates through a different method, a one-time PIN, say, access control may no longer evaluate that user's IdP group memberships at all, because group membership data only persists through IdP-based authentication flows specifically. Switching the method may cause the system to quietly lose context about who that user is in relation to their groups, even though nothing about the login itself looked like a failure. That gap doesn't appear in a security audit until someone goes looking for it directly, and it's a reminder that continuous verification is only as strong as the identity signal it's continuously checking against.

Sources

  1. Session Security in 2025: What Works for Cookies, Tokens, and Rotation
  2. Browser Cloud Auth State Across Parallel Sessions
  3. theneuralbase.com
  4. JWTs vs. sessions: which authentication approach is right for you?
  5. clerk.com
  6. en.wikipedia.org
  7. pivotpointsecurity.com
  8. zylos.ai
Filed underBrowser Sessions

More in Browser Sessions