Library
8 min read

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.

Your answer

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#

http
Set-Cookie: sid=9f3c1a7e5b2d4c8a; HttpOnly; Secure; SameSite=Lax; Path=/
 
# On the server, in Redis:
#   sid:9f3c1a7e5b2d4c8a -> { userId: 42, role: "editor", createdAt: ... }  TTL 7 days

A 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.

Where the truth lives
The server holds the truth. Delete the row and the user is out on their next request. The price is a network round trip to the store on every request, and the store must be up.

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.

Revoke a user, then keep clicking
Access token lifetime
3 min
Session id in a cookie
Server looks the id up in Redis on every request.
401, locked out
The admin deleted the session row. The very next request finds nothing.
Cost: 1 Redis read per request (about 0.3 ms)
JWT, verify signature only
Server checks signature and exp. No lookup.
200 OK, still in
The signature is still valid and exp is 11 min away. Nothing on the server knows about the revoke.
Cost: No lookup at all
JWT + denylist of revoked ids
Verify signature, then check the token id against a revoked list.
401, locked out
Stopped at once, but you are now doing a lookup per request. You have rebuilt sessions with extra steps.
Cost: 1 Redis read per request, again
3 min after the revoke, the plain JWT still works for another 11 min. Your worst case is always the access token lifetime, which is why access tokens are kept short and paired with a refresh token you can revoke.

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 jti claim) 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.

ChoiceWhat you gainWhat you payPick it when
Server side sessionsInstant 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 tokensAny 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 insideThe 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#

Interviewer
JWTs are stateless, so they scale better than sessions. Agree?
A deliberately one sided statement. They want the counterweight.
You
Partly. Verifying a JWT needs no lookup, which helps when many services must authenticate requests. But a Redis session lookup is a fraction of a millisecond and Redis handles hundreds of thousands of reads a second, so for most apps the lookup is not the bottleneck. And the moment you need real logout, you add a denylist and the state is back. I would use sessions for a web app with one backend, and short lived JWTs with refresh tokens when there are many services or third parties verifying tokens.
Puts a number on the cost of a session, then says when each wins.
Interviewer
A user changes their password. How do you log out their other devices?
You
Revoke all their refresh tokens, so no new access tokens get issued, and store a per user timestamp. Any access token issued before it is rejected. That costs one cached lookup per request, but it is per user, not per token, so it stays small.
True or false?
The payload of a JWT is encrypted
Deleting a user's row makes their JWT stop working
A session id should be random and meaningless
You can put the user's role in a JWT

Checkpoint#

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?

Worth memorising
  • 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 alg from the token.
Say this in 60 seconds

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.