ML
Security

Sessions vs JWTs: Stateful Cookies, Stateless Tokens, and the Logout You Can't Actually Do

A session is a pointer to server state; a JWT is the state itself, signed. That single difference decides revocation, scaling, and where your security holes live. The trade-off, honestly told.

June 15, 20269 min readSecurity

"Should I use sessions or JWTs?" gets answered with cargo-culted opinions far more often than with the actual trade-off. The honest version is one sentence: a session is a pointer to state your server holds; a JWT is the state itself, signed so the client can hold it. Every difference that matters — revocation, scaling, blast radius — falls out of that. Here it is properly.

Sessions: the server remembers

On login, the server creates a record (user id, roles, expiry) in a store it controls — Redis, a database table — keyed by a random, opaque session_id. Only that meaningless id goes to the client, in a cookie:

Set-Cookie: sid=9f2a...e1; HttpOnly; Secure; SameSite=Lax

// every request: server looks the id up
session = store.get("9f2a...e1")   // -> { userId: 42, roles: [...] }

The cookie carries no information — steal it and you learn nothing; it is just a lookup key. The truth lives server-side, which has one enormous consequence: the server can change or delete it at any instant.

JWTs: the client carries the truth

A JWT puts the claims in the token and signs them. Three base64url parts — header, payload, signature — joined by dots:

eyJhbGci...   header   { "alg": "HS256", "typ": "JWT" }
eyJzdWIi...   payload  { "sub": 42, "roles": ["admin"], "exp": 1718... }
SflKxwRJ...   signature  HMAC/RSA over the first two parts

The server verifies the signature with a key and trusts the payload — no database lookup. That is the whole appeal: any instance with the key can validate a request statelessly, which scales beautifully and removes a round-trip. Two things people get wrong immediately: the payload is signed, not encrypted — anyone can base64-decode and read it, so never put secrets in it. And the signature only proves "we issued this and it's unchanged"; it says nothing about whether the claims are still true.

The revocation problem nobody mentions up front

Here is the catch that the "JWTs are better" crowd skips. A user clicks "log out everywhere", or you fire an admin, or a token leaks. With sessions you delete the record and the next request fails instantly — done. With a JWT there is nothing to delete: the token is valid, by definition, until its exp. The server isn't consulting any state, so it cannot know you want this one dead. Stateless auth and instant revocation are fundamentally in tension.

The standard mitigation is the two-token dance: a short-lived access token (5–15 min, stateless, used on every request) plus a long-lived refresh token (stored server-side, checked only when minting a new access token). Revocation means deleting the refresh token; the access token still works until it expires, so your real logout window is the access-token lifetime, not instant. If you cannot tolerate that window you add a server-side denylist of revoked token ids — at which point you've reintroduced a lookup on every request and rebuilt sessions with extra steps. Be honest about which one you're actually running.

Where the security holes live differs

The two models fail in different places, and choosing one is choosing which class of bug to defend against:

  • Cookie/session — vulnerable to CSRF (the browser auto-attaches the cookie to forged cross-site requests). Defended by SameSite and CSRF tokens. The cookie itself should be HttpOnly so JavaScript — and thus XSS — can't read it.
  • JWT in localStorage — the popular tutorial setup, and the dangerous one: it's readable by JavaScript, so any XSS exfiltrates the token directly. People pick JWTs to "avoid CSRF" and walk into a worse XSS exposure. Storing the JWT in an HttpOnly cookie instead gives you the better storage of the session model — but then CSRF is back and you've kept only the stateless-verify benefit.

And alg: none / algorithm-confusion bugs are JWT-specific: libraries that honoured an attacker-supplied alg let forged tokens through. Always pin the accepted algorithm server-side; never trust the header's.

So which one

Default to sessions for a normal web app with a backend you control — instant revocation, opaque cookies, decades of hardened tooling, and a Redis lookup is microseconds. Reach for JWTs when statelessness genuinely pays: many services validating without a shared session store, or third parties verifying with just your public key. Use short-lived access plus refresh tokens, store them in HttpOnly cookies, and go in knowing you've traded instant logout for stateless scale — not gotten both.

Rules of thumb

  • Session = pointer to server state (revocable instantly, needs a lookup). JWT = signed state in the client (stateless, hard to revoke). Pick the trade-off deliberately.
  • A JWT payload is signed, not encrypted — readable by anyone. No secrets in it, ever.
  • You can't truly revoke a stateless JWT before exp. Use short access + server-side refresh tokens; accept that real logout lag equals the access-token lifetime.
  • JWT in localStorage trades CSRF for XSS token theft — usually a bad trade. Prefer HttpOnly cookies for either model.
  • Pin the accepted signing algorithm server-side; reject alg: none and never trust the token's own alg header.
  • If you bolt a revocation denylist onto JWTs, you've rebuilt sessions with extra steps — at that point just use sessions.
SharePostLinkedIn

Reader Discussion

2 replies// weighed in

TopNewestAuthor
Add to the thread
Disagree, agree harder, or share your own experience…
Email instead →markdown okbe kind
  1. Rachel Gold· Staff SREAgrees

    the on-call framing throughout this piece is what makes it land. too many infra articles assume you never get paged. those are written by people who never got paged.

    Jun 18, 2026·3 days later
  2. Omar Khalil· Senior SWEKind words

    this is the third article from this blog I've sent to my team this month. you're cooking. don't switch to crypto.

    Jun 20, 2026·5 days later

Worked on something similar? Email ducminhldm@gmail.com — I read every one. The good ones become future posts.

Comments seeded · live discussion via email