Skip to content
Security

JWTs vs Sessions: The Debate, Settled-ish

Why revocation is where stateless goes to die, the XSS blast-radius problem, the specific habitats where JWTs genuinely shine, and the sessions-at-the-edge / tokens-between-machines architecture.

4 min read Updated Sep 3, 2026
JWTs vs Sessions: The Debate, Settled-ish

Somewhere around 2015, the industry collectively decided sessions were legacy and JWTs were the future, and a decade of security incidents has been quietly walking that back ever since. The honest version: server-side sessions remain the right default for almost every web application, and JWTs are a specialized tool for federation and service-to-service trust — brilliant at their actual job, hazardous when hired for the wrong one. Let me make that case with mechanisms, not vibes.

Server-side session store versus stateless JWT flow, with revocation and refresh in the middle

What each thing actually is

Session: server stores state (user ID, roles, metadata) keyed by a random opaque token in an httpOnly cookie. The token means nothing; all truth lives server-side, one lookup away — in Redis or the database, both effortless at any scale short of planetary.

JWT: a signed (not encrypted — base64 is not privacy!) package of claims the client holds. The server validates the signature and trusts the contents without any lookup. That statelessness is the entire feature — and the entire problem, because a thing you can't look up is a thing you can't take back.

Revocation: where stateless goes to die

The question that decides most arguments: the user clicks "log out of all devices" — or you disable a compromised account — what happens? Sessions: delete the rows; done, globally, instantly. JWTs: every already-issued token remains valid until expiry, because validity lives in mathematics, not in your database. The industry's workarounds form a comedy in three acts: short expiries (now you need refresh-token machinery — which is server-side state), token blocklists (checked on every request — congratulations, you've rebuilt session lookups with extra steps), and "just rotate the signing key" (mass-logout of every user; the nuclear option). If your system needs the ability to end a login right now — and account takeover response means it does — you need server-side state somewhere. At which point the "stateless" premium you paid bought nothing.

The XSS blast-radius problem

Where does the client keep the token? Sessions ride in httpOnly, Secure, SameSite cookies — invisible to JavaScript, so an XSS foothold can't exfiltrate them (CSRF is the cookie trade-off, and SameSite plus framework CSRF tokens — Laravel ships this wired — handle it). JWTs in practice land in localStorage or JS-readable memory, where any XSS reads them and walks away with a bearer credential valid until expiry, usable from anywhere. You can put JWTs in httpOnly cookies too — at which point, again, you've rebuilt cookie-based auth with a fatter token and no revocation. The pattern repeats: every hardening of JWT-for-web converges back toward sessions.

Where JWTs genuinely shine

None of the above means JWTs are bad — it means their habitat is specific:

  • Federation between parties. The ID token in OIDC — identity asserted across trust boundaries where a shared session store can't exist. This is what JWTs were for.
  • Service-to-service and API gateways. Short-lived (minutes) access tokens carrying claims between internal services — the gateway validates once, downstream services trust the signature, nobody shares a session store. Short expiry makes revocation moot; statelessness pays real rent here.
  • Delegated, scoped, expiring capability — signed URLs, webhook auth, "this mobile client may call these three endpoints for 10 minutes" — anywhere the token is the credential and its short life is the security model.

The architecture that follows: sessions at the human edge, JWTs between machines. Your Laravel/. NET web app authenticates browsers with cookie sessions; your gateway mints five-minute JWTs for the service mesh behind it. Both tools, both in their habitat.

Implementation notes for whichever you run

Sessions:  httpOnly + Secure + SameSite=Lax minimum; regenerate ID on login
           (fixation) and privilege change; Redis store with absolute + idle
           timeouts; "active sessions" UI with per-device revoke = user trust.

JWTs:      alg allowlist server-side (the alg:none and RS256→HS256 confusion
           attacks live forever); validate iss/aud/exp ALWAYS; asymmetric keys
           (RS256/EdDSA) so verifiers can't mint; kid + JWKS for rotation;
           expiry ≤ 15 min for access tokens; refresh tokens are server-side
           state — rotate them on use and detect reuse (token theft alarm).

Both:      the token is not the authorization. Roles/permissions checked at
           use-time from source of truth — a stale "admin" claim in a valid
           token is exactly the incident you think it is.

The meta-lesson outlives the debate: statelessness is a trade, not a virtue. You exchange lookup costs (tiny, cacheable) for control costs (revocation, rotation, blast radius). At the human login edge, control wins — it always did. The pendulum's return to boring sessions isn't regression; it's the industry finishing the experiment and writing down the result.

Auth architecture reviews — session hardening, token flows, the whole OAuth stack — are standard fare in my consulting, ideally before the pentest report makes the argument for me.

Keep reading

Related articles

Security 5 min read

Secrets Management: Life Beyond the .env File

The real leak vectors ranked, the non-negotiables at any size, a three-rung maturity ladder that mostly stops at rung two, and rotation as a rehearsed muscle with two-key overlap.

Security 5 min read

OAuth2 and OIDC in Plain Words

The founding delegation story, the cast renamed into English, the one flow that survived (auth code + PKCE) with every step's attack labeled, ID vs access tokens untangled, and the do-you-even-need-this triage.