Home / Blog / JWT vs Session: real decision criteria, not tribal debate

JWT vs Session: real decision criteria, not tribal debate

The internet is split down the middle on this. A clear framework for the questions that actually matter when you pick.

Every two years the same Twitter fight flares up: “don’t use JWT” vs “sessions are stateful, they don’t scale”. Both arguments are shallow. The real question is: what does your use case actually demand?

I’ve shipped both models across dozens of backend projects. This post isn’t here to end the debate, it’s here to help you make the right call for your specific situation.

What sessions are (and aren’t)

You keep a session store on the server (Redis, memcached, or a DB). When a user logs in, you generate a random session ID and drop it into their cookie. On every request the client sends the cookie, the backend looks the ID up in the store and finds the user.

Upsides:

  • Revocation is trivial: delete from the store and the user is logged out immediately
  • Small payload: the cookie is just an ID
  • Updating user info is easy: change the store, the next request sees the new values

Downsides:

  • The store is looked up on every request, adding latency and load
  • Distributed systems need sticky routing or a shared store
  • Cross-domain is awkward (cookie limitations)

What JWT is (and isn’t)

On login the backend mints a token that contains user info (id, role, expiry). The token is cryptographically signed, so the backend can tell if it was tampered with. The client sends the token on every request, usually as Authorization: Bearer.

Upsides:

  • Stateless: the backend stores nothing, it just verifies the signature
  • Natural fit for distributed systems: any server can verify
  • Cross-domain and cross-service are easy
  • No DB lookup, the info is already in the token

Downsides:

  • Revocation is hard: the token still “looks valid”
  • Tokens are big (500 bytes to 2KB), bandwidth on every request
  • User info changes don’t update the token (old info until expiry)
  • If a token leaks, there’s nothing you can do until it expires

The decision tree

Ask yourself about your specific situation:

Question 1: Do you need instant revocation?

Banking, medical, payroll, any sensitive system. When a user logs out, or a breach is detected, that session has to die now.

  • Yes → Session. To do this with JWT you’d keep a blacklist store, at which point the stateless advantage is gone.
  • “A 15-minute delay is acceptable” → JWT can work (short expiry + refresh token).

Question 2: Do you run multiple services?

If you have 3+ backend services all authenticating the same user:

  • Monolith → Session is probably enough
  • Microservices → JWT, each service verifies independently
  • Hybrid (API gateway in front) → the gateway validates JWT, inside you keep a session-like state, but JWT is still useful at the edge

Question 3: Do you have a mobile or native client?

Cookie-based sessions are awkward on native mobile. HTTP clients don’t always handle cookies well. Token-based auth is more comfortable on native.

  • Web only → Session is simpler
  • Mobile + web → JWT is more universal

Question 4: How often does user info change?

Say a user just upgraded to premium:

  • Session: next request the premium feature is live (store is updated)
  • JWT: the old token is still valid until reissue, and the user wonders “why didn’t premium turn on?”

“Rarely changes” → JWT is tolerable
“Permission changes have to be instant” → Session

Hybrid approach: the best of both

The pattern I use most often in real production deployments:

  1. Access token (JWT): short-lived (15 minutes), stateless, sent with every API request
  2. Refresh token (opaque, in DB): long-lived (30 days), only used to mint a new access token

Flow:

  • Login → access token + refresh token
  • Every API request carries the access token
  • When the access token expires, the client uses the refresh token to get a new one
  • The refresh token is revocable (delete from DB)
  • Sensitive operations (password change, payment) require re-authentication

This pattern keeps JWT’s statelessness where it matters, while preserving session-style revocation. Stripe, GitHub, Google, all use a variant of this.

Common misconceptions about JWT

1. JWT is signed, not encrypted

The payload is base64 encoded, not encrypted. Anyone with the token can read its contents. Never put sensitive data (credit cards, passwords) inside a JWT. Only user_id, role, expiry, that class of thing.

If you genuinely need encryption, use JWE (JSON Web Encryption). You almost never need to.

2. The “JWT is more scalable” myth

Using JWT doesn’t automatically make you scalable. If your session store is Redis, you’re scalable already. “Stateless scales better” is a theoretical difference that only shows up north of 100 servers.

3. Storing JWT in localStorage

XSS-vulnerable. JavaScript in the browser can steal the token. Put it in an HTTP-only cookie (XSS-safe). At that point the gap between JWT and sessions narrows a lot: a cookie-delivered JWT behaves session-like.

Takeaway

The right answer is “it depends”. But “it depends” isn’t useful on its own; you need to know which questions to ask:

  1. How critical is revocation speed?
  2. How distributed is the system?
  3. What are the client types (web, mobile, third party)?
  4. How often does user info change?

Answer those four and the right model becomes obvious for your case.

My personal default? Session cookies for most web SaaS. JWT + refresh-token pattern when mobile, web, and third-party integrations are in the mix. JWT is mandatory for microservices. Hybrid works anywhere.

What matters is deciding from the practice, not from the principle.

Have a project on this topic?

Leave a brief summary — I’ll get back to you within 24 hours.

Get in touch