DocumentsBetter Auth
Session Management
Session Management
Type
Topic
Status
Published
Created
Jul 8, 2026
Updated
Jul 8, 2026
Created by
Dosu Bot
Updated by
Dosu Bot

Session Management#

Session management in better-auth encompasses three orthogonal concerns: deployment mode (stateful with a durable store vs. stateless cookie-only), session validation strategy (cookie-cache bypass vs. DB-backed lookup), and runtime initialization of the AsyncLocalStorage context used to thread session state through request handlers.

The central function for resolving a session from an incoming request is getSessionFromCtx in packages/better-auth/src/api/routes/session.ts. Whether it hits the cookie cache or the database depends entirely on the deployment mode detected at runtime.

Stateful vs. Stateless Deployment Modes#

The single source of truth for deployment-mode detection is hasServerSessionStore() in packages/better-auth/src/context/store-capabilities.ts:

hasServerSessionStore(options) → !!options.database || !!options.secondaryStorage

A deployment is stateful if either a database or secondaryStorage is configured; otherwise the session lives entirely in the signed session cookie. Note that hasServerAccountStore() (used to decide whether to bind account cookies to session users) requires database only — secondary storage alone is not enough .

The isStateful(ctx) helper (imported in routes from session.ts) is used throughout token and session routes to gate cache-bypass and fail-closed behavior.

/get-access-token, /refresh-token, and /account-info all resolve their session via resolveUserId, which calls getSessionFromCtx with disableCookieCache: isStateful(ctx) . This ensures a server-side session revocation takes effect immediately rather than waiting for the cached cookie to expire. In DB-less deployments, the cookie remains authoritative and the cache is not bypassed.

getSessionFromCtx cannot be weakened by query params#

When disableCookieCache or disableRefresh is forced by a caller, the merge logic ORs the config value with the request query param , preventing a crafted ?disableCookieCache= request from overriding a forced strict validation setting.

/update-session fails closed on revoked sessions#

After attempting to update the session row, if no row was updated (updateSession returns null) and the deployment is stateful, /update-session deletes the session cookie and throws UNAUTHORIZED . This prevents re-minting a revoked session from stale cookie data. Stateless deployments are unaffected — the cookie is the only state, so a missing DB row is normal.

Stateless Cross-Instance Account Resolution#

Problem (PR #9979): In stateless OAuth deployments, sign-in and subsequent requests can land on different server instances. The account cookie's userId is bound to the instance that performed sign-in; the session cookie resolved by a different instance carries a different local userId. With no durable account row, the strict equality check fails, breaking /get-access-token, /refresh-token, /account-info, and session-cache refresh.

Fix: shouldBindAccountCookieToSessionUser() gates strict user-id binding on hasServerAccountStore() (database only). Without a database, account selection falls back to provider identity (and optional provider account ID). Additionally, setCookieCache no longer clears the account cookie when userId mismatches in stateless mode, preventing spurious "Account not found" errors on session refresh.

AsyncLocalStorage Concurrency Race on Cold Isolates#

Issue #10300 — affects v1.6.x including v1.6.23.

Root cause#

ensureAsyncStorage in packages/core/src/context/request-state.ts uses an unguarded check-then-await-then-assign pattern:

  1. Caller A and Caller B both see requestStateAsyncStorage == null and enter the if block.
  2. A awaits getAsyncLocalStorage(), yielding to the event loop.
  3. B completes its await, assigns a new AsyncLocalStorage instance to the global, overwriting the instance A is already running inside.
  4. When A's handler resumes, getCurrentRequestState() queries the new instance — which has no store — and throws "No request state found".

The same unguarded pattern exists in all three async storage initializers: requestStateAsyncStorage , endpointContextAsyncStorage , and adapterAsyncStorage — all need the same fix.

Observable symptoms#

  • Intermittent INTERNAL_SERVER_ERROR: No request state found on cold isolate startup.
  • getSession() returns null for a logged-in user → spurious 401s downstream.
  • Self-heals after the first burst (the race only occurs during first-init).
  • Most commonly triggered by SSR frameworks fanning out parallel auth.api.getSession() calls per render on Cloudflare Workers or similar edge runtimes.

Fix (PR #9862 — open as of 2026-07-02)#

Promise memoization + idempotent ??= assignment: all concurrent callers await the same init promise, and the ??= operator ensures a completed instance is never overwritten. A regression test races 32 concurrent requests through initialization.

Workaround until merged: Ensure the first auth.api.getSession() call on a cold isolate completes before dispatching concurrent requests (e.g., serialize the first call in a warmup handler).

Key Files & References#

FilePurpose
packages/better-auth/src/api/routes/session.tsgetSessionFromCtx, getSession endpoint, isStateful helper
packages/better-auth/src/api/routes/account.tsresolveUserId — cache-bypass for token routes
packages/better-auth/src/api/routes/update-session.tsFail-closed revocation logic
packages/better-auth/src/context/store-capabilities.tshasServerSessionStore, shouldBindAccountCookieToSessionUser
packages/core/src/context/request-state.tsensureAsyncStorage, runWithRequestState, getCurrentRequestState
packages/core/src/context/endpoint-context.tsendpointContextAsyncStorage initializer (same race pattern)
packages/core/src/context/transaction.tsadapterAsyncStorage initializer (same race pattern)

Related PRs:

  • PR #9967 — Honor server-side session deletion in /update-session and token routes (merged 2026-06-10)
  • PR #9979 — Stateless account cookie resolution across instances (merged 2026-06-11)
  • PR #9862 — AsyncLocalStorage concurrency fix (open as of 2026-07-02)
  • Issue #10300 — AsyncLocalStorage race report and analysis