Every auth discussion eventually hits “should we use JWTs or sessions?” The internet’s answer is “it depends.” That’s true and unhelpful. Here’s a more useful take.
The real difference
- Session: a random ID stored in a cookie. The server looks up the ID in a database or cache to find the user.
- JWT: a signed token containing the user’s claims. The server verifies the signature and reads the claims directly.
JWTs are often described as “stateless” because the server doesn’t need to query a database on each request. That’s half true. In practice, most JWT setups add state back in (revocation lists, key rotation tracking, step-up auth flags) and end up with the worst of both worlds.
When sessions win
- You need revocation. Signing a user out, kicking a compromised device, or booting a fired employee requires deleting a row. Trivial with sessions. Hard with JWTs.
- You need to change user state. Role changes, permission updates, tenant switches — all handled by reading fresh data from the DB on each request. With JWTs you either re-issue tokens on every change (defeating the point) or accept that users have stale permissions until their token expires.
- You’re on a single backend. A session lookup is a cheap DB query. Premature optimization here is a classic trap.
When JWTs win
- You’re passing the token across service boundaries you don’t control. The canonical case: your frontend hits your API, your API hits a microservice, the microservice needs to know who the user is. A JWT works because verification only needs the public key. Sessions would need a central lookup.
- You’re in a federated identity setup. OIDC hands you an ID token (a JWT). Verifying it against the IdP’s JWKS is simpler than querying the IdP for every request.
- You’re explicitly designing for offline or delayed verification. Devices that sync when they reconnect can still verify a JWT without a network call.
The actual recommendation
For most SaaS, use sessions backed by a Secure; HttpOnly; SameSite=Lax cookie. Store session rows in Postgres or Redis. Short (minutes-long) JWT access tokens are a reasonable addition if you’re calling your own APIs from a SPA — but the session is what actually tracks the user.
Start with sessions. Add JWTs when the architecture demands them, not because you read a blog post.