JWT vs Session Authentication: Where the State Lives
Both approaches answer the same question on every request: which user is this? The disagreement is over where the answer is kept. A session keeps it on the server and sends the browser a claim ticket. A JWT writes the answer down, signs it, and hands the whole thing to the browser.
Everything else people argue about — scaling, revocation, token size, mobile apps — is a consequence of that one decision.
DELETE that takes effect immediately; revoking a JWT is a wait.What a session actually is
The server stores a record — user id, expiry, maybe a device — and gives the browser a random opaque id in a cookie. The cookie means nothing on its own. On each request the server looks the id up, finds the record, and knows who is calling.
That lookup is the thing people want to remove. It is also the thing that makes everything else easy: to log someone out everywhere, delete their rows; to force re-login after a password change, delete their rows; to see who is signed in right now, read the table.
What a JWT actually is
Three base64 segments: a header, a payload of claims, and a signature over the first two. The server verifies the signature with a key it already holds, and if it verifies, trusts the claims inside.
The critical property, and the one most often missed: a JWT is signed, not encrypted. Anyone holding the token can read every claim in it.
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyLTMxIiwiZXhwIjoxNzg2MH0.4vQ…
└──── header ────┘ └──────── claims, readable ────────┘ └ sig ┘
The trade-off, stated honestly
Sessions cost a read per request. In practice that read is against Redis and takes well under a millisecond, which for the overwhelming majority of applications is not a problem anyone would have noticed if they had not gone looking for it.
JWTs cost revocation. There is no server-side record, so there is nothing to
delete. A stolen token, a fired employee, a password reset — none of them
invalidate anything. The token works until exp.
The standard fix is a denylist of revoked token ids, checked on every request. This works, and it is worth noticing what it is: a database read per request. You have arrived back at sessions, with extra steps and a larger cookie.
So when are JWTs the right call
They earn their keep when the verifying party cannot reach your session store:
- Service to service. A downstream service can verify a signature without a network call to your auth service. This is the strongest case, and it is why the pattern exists.
- Across trust boundaries. OAuth access tokens, federated identity, a partner’s API validating tokens you issued.
- Genuinely large scale, where a per-request read against a shared store is a measured bottleneck rather than an assumed one.
For a single web application with one backend and one database, sessions are the better default. The lookup is cheap, and instant revocation is worth more than removing it.
The pattern that works in practice
Use both, for what each is good at:
- A short-lived access token — five to fifteen minutes. Stateless, verified locally, no lookup. Its short life is the revocation window.
- A long-lived refresh token — stored server-side, one row per session, revocable. Used only to mint new access tokens.
Logging someone out deletes the refresh row. Their access token stays valid for its few remaining minutes, and that bounded window is the whole compromise. If minutes is too long for your risk profile, you do not want stateless tokens at all — you want sessions, and you should say so rather than bolting a denylist onto a JWT and calling it stateless.
Rotate refresh tokens on every use, and treat reuse of an already-consumed refresh token as theft: revoke the whole family. That single rule catches most real-world token compromise.
Storage, which is where the actual breaches happen
Whatever you choose, put it in a cookie with HttpOnly, Secure and
SameSite=Lax (or Strict). localStorage is readable by every script on the
page, so one XSS anywhere — a dependency, an analytics snippet, a comment
widget — is a total credential compromise. HttpOnly removes that class of
attack entirely, and it costs nothing.
The common objection is that mobile apps and cross-origin SPAs cannot use
cookies. Mobile apps should use the platform keychain, which is better than
either. Cross-origin SPAs can use cookies with correct CORS and SameSite=None; Secure — it is configuration, not an impossibility.
What you have actually decided
Not “stateless versus stateful” — a system with users is stateful by definition, and the state does not disappear because you moved it into the client’s pocket.
What you decided is who holds the truth and how fast you can change your mind about it. Sessions keep it and can change it instantly. JWTs lend it out and cannot take it back before it expires. Pick the one whose failure mode you can live with, and size the expiry to how long you are willing to be wrong.
Quick answers
- Is JWT better than session authentication?
- Neither is better in general. JWTs remove a database read per request, which matters at high volume or across services that cannot share a session store. Sessions keep revocation instant and simple. For a single web application, sessions are usually the better default.
- Why can't you revoke a JWT?
- Because nothing is stored server-side to delete. The token is valid if its signature verifies and it has not expired, so a server with no extra state has no way to say "not this one". Adding a denylist works, but it reintroduces the per-request lookup that JWTs existed to avoid.
- Should JWTs be stored in localStorage or cookies?
- Cookies, marked HttpOnly, Secure and SameSite. Anything in localStorage is readable by any JavaScript that runs on the page, so a single XSS flaw hands over the token. HttpOnly cookies are not readable by script, which removes that entire class of theft.
- How long should a JWT last?
- Minutes, not days. The expiry is your only revocation mechanism, so it is also your worst-case exposure window after a token is stolen. Pair a short access token with a longer refresh token that is stored server-side and can actually be revoked.
References
Related Discoveries
Lumi's weekly note
A short email when we publish something useful. No spam, unsubscribe anytime.