Sessions vs JWTs
Published
At a wedding in Jaipur you hand your shoes to the attendant at the door and get a token with a number on it, 47. The token means nothing by itself. To get your shoes back, the attendant looks up 47 on the rack. That is a session: you carry a meaningless id, and the server keeps the real information.
At a concert, you get a wristband instead. Printed on it: "VIP, 18 Oct, valid until 23:00", with a hologram that is hard to fake. The bouncer does not look anything up. He reads the band, checks the hologram, and lets you in. That is a JWT: you carry the information itself, signed so nobody can change it.
Both are ways to remember that you already logged in. The difference is where the truth lives, and that difference decides almost everything else.
An employee is fired at 3 pm. Your app uses JWTs with a 24 hour expiry and checks only the signature. The admin deletes the account. Until when can the employee keep using the app, and why?
What each one looks like#
Set-Cookie: sid=9f3c1a7e5b2d4c8a; HttpOnly; Secure; SameSite=Lax; Path=/
# On the server, in Redis:
# sid:9f3c1a7e5b2d4c8a -> { userId: 42, role: "editor", createdAt: ... } TTL 7 daysA session id is random and opaque, 128 bits of nothing. The server must look it up on every request. A JWT (JSON Web Token, said "jot") is three base64url parts joined by dots: a header, a payload of claims, and a signature. Any service holding the issuer's public key can check the signature and trust the claims without asking anyone.
Two things people get wrong about JWTs. First, the payload is encoded, not encrypted.
Paste one into any decoder and read it. Never put anything secret in there. Second, the
signature makes it tamper evident, not secret: change editor to admin and the
signature no longer matches.
The logout problem#
Here is the trade in one sentence: a JWT is fast because the server does not look anything up, and it is hard to revoke for exactly the same reason. Try it. Revoke the user, then move the clock forward.
This is the real answer to "why not just use JWTs everywhere?". Logging out, a password change, a fired employee, a stolen laptop: all of them need the server to say "no" to a token that is still cryptographically valid. Your options:
- Keep access tokens short, 5 to 15 minutes, and pair them with a refresh token the server stores and can revoke. The worst case shrinks to one access token lifetime. This is what nearly everyone does. See access vs refresh tokens.
- A denylist of revoked token ids (the
jticlaim) in Redis, checked on each request, with each entry expiring when the token would have. It works, and it reintroduces the lookup JWTs were meant to avoid. - A per user "tokens valid after" timestamp, checked against the token's
iat. One small lookup that logs a user out everywhere at once.
Other things that bite#
Size. A session cookie is about 40 bytes. A JWT with a few claims is 400 to 800 bytes, and it rides on every request. Stuff permissions into it and you hit the browser's 4 KB cookie limit, or proxies that cap headers at 8 KB.
Stale claims. The role in the token is the role at login. Promote someone to admin and
their token says editor until it expires.
Algorithm confusion. Early JWT libraries trusted the alg in the header. An attacker
could set "alg": "none" and send no signature, or switch RS256 to HS256 and sign with the
public key as if it were a shared secret. Always pin the expected algorithm on the server.
| Choice | What you gain | What you pay | Pick it when |
|---|---|---|---|
| Server side sessions | Instant logout and revocation, tiny cookie, claims always fresh, nothing sensitive leaves the server. | A store lookup on every request, and the store becomes something that must scale and stay up. | A web app with its own backend. This is the right default for most products. |
| JWT access tokens | Any service verifies locally, no shared store, works across domains and for third party APIs. | Revocation is hard, tokens are large, claims go stale. | Many services or many organisations need to trust one issuer: microservices, public APIs, OIDC. |
| Both: session at the edge, JWT inside | The browser has a revocable session cookie. The gateway swaps it for a short JWT that internal services verify. | One more moving part at the gateway. | Larger systems with a web frontend and many backend services. |
In the interview#
Checkpoint#
1. A fired employee holds a JWT with 24 hour expiry, and the server only checks the signature. How long can they keep using it?
2. Which is a genuine advantage of JWTs over sessions?
3. How do you defend against the "alg: none" attack?
- Session: opaque id, truth on the server, instant revoke, a lookup per request.
- JWT: signed claims, truth in the token, local verify, revoke is hard.
- JWT payloads are readable. Signed is not encrypted.
- Short access tokens (5 to 15 min) plus revocable refresh tokens is the standard compromise.
- Pin the algorithm. Never trust
algfrom the token.
A session is an opaque random id in a cookie; the server looks it up in a store like Redis on each request, so the server holds the truth and can revoke it instantly. A JWT is a signed set of claims the client carries; any service with the issuer's public key verifies it locally without a lookup. That makes JWTs good when many services or third parties must trust one issuer, but it means the server has nothing to delete, so a token stays valid until it expires. The standard compromise is short lived access tokens, five to fifteen minutes, with refresh tokens stored server side that can be revoked, plus a denylist or a per user cutoff when instant logout matters. JWT payloads are readable, can go stale and are large, and the server must pin the algorithm. For a typical web app with its own backend I use sessions.