Backend / Authentication / JWT / 07_jwt_vs_session.md

JWT vs Session-Based Auth

Updated 7 interview angles 7 min read source
On this page12
  1. The mechanics, side by side
  2. Side-by-side comparison
  3. When to choose sessions
  4. When to choose JWT
  5. The hybrid pattern — best of both
  6. The “JWT is stateless” claim — qualified
  7. XSS vs CSRF — different threats
  8. Performance comparison (rough)
  9. Migration paths
  10. Common pitfalls (when picking one)
  11. Common interview confusions
  12. Interview angle

JWT vs Session-Based Auth

The classic interview question. JWTs are popular partly because they’re new and partly because they solve real problems — but they’re not always the right tool. Sessions are simpler and often safer.

For broader stateful-vs-stateless context: Stateful and stateless.

The mechanics, side by side

Session-based

text
1. Login:        POST /login {user, pass} → server validates
2. Server:       creates session in Redis: {sess_xyz123 → {user_id: 42, ...}}
                 sets cookie: Set-Cookie: session=sess_xyz123; HttpOnly; Secure
3. Each request: Cookie: session=sess_xyz123
4. Server:       Redis lookup: redis.get("sess:xyz123") → user data
5. Logout:       redis.delete("sess:xyz123"); clear cookie

The session ID is an opaque random string. State lives in Redis. Server is stateful (via Redis).

JWT-based

text
1. Login:        POST /login {user, pass} → server validates
2. Server:       creates JWT: jwt.encode({sub: 42, exp: ...}, secret)
                 returns it; client stores
3. Each request: Authorization: Bearer eyJ...
4. Server:       jwt.decode(token, key) → payload (signature check, exp check)
                 no DB lookup
5. Logout:       (the hard part — see [05_revocation_logout.md](05_revocation_logout.md))

The token IS the state. Server is stateless for verification.

Side-by-side comparison

Session-based JWT-based
Storage server (Redis / DB) client (cookie / localStorage)
Verification cost Redis/DB lookup per request local crypto verification
Revocation trivial (delete from Redis) hard (blocklist, short tokens, version)
Scaling shared session store needed stateless (any server can verify)
Cross-service auth each service hits session store each service has the public key
Token size in transit small (~30 chars cookie) larger (~500-1000 bytes)
Privacy server-side (DB only) claims readable by anyone with token
Mid-session updates trivial (update Redis) wait for next refresh
CSRF yes (cookies sent automatically) depends (header-based usually no)
XSS impact session ID via cookie HttpOnly = safer localStorage = exposed; cookie HttpOnly = safer

When to choose sessions

  • Single domain / monolith: simplicity wins.
  • Easy revocation matters: logout, force-logout, ban a user — instant.
  • Roles/permissions change mid-session: instant reflection.
  • You already have Redis: marginal cost.
  • Privacy of identity matters: nothing in the cookie except an opaque ID.

The classic Rails / Django default. Works for most apps. Don’t over-engineer with JWT if sessions cover your needs.

When to choose JWT

  • Multiple services verify: microservices, federated systems. Each service has the public key; no shared session store.
  • Cross-domain auth: SSO, OIDC (ID tokens are JWTs).
  • Serverless / Lambda: instances die between requests; can’t hold session state.
  • Massive scale: per-request Redis lookups become a bottleneck (premature optimization for most teams).
  • Multi-region active-active: writing sessions to one region’s Redis and reading from another is painful; stateless tokens transit unchanged.
  • Native mobile: cookies are awkward; bearer tokens are cleaner.

Note: most of these are “you have specific reasons.” For a typical B2B SaaS in one region with one frontend, sessions are simpler.

The hybrid pattern — best of both

Many production systems combine:

text
1. User logs in
2. Server: creates session in Redis (server-side state)
3. Server: issues a signed cookie pointing at the session
4. Each request: cookie → Redis lookup → user
5. For service-to-service: the API service issues a short-lived JWT
   signed by an internal key, containing { sub, scopes, exp: 5min }
6. Downstream services validate the JWT locally

The user-facing auth is session-based (easy revocation, no PII in token). Internal service-to-service auth uses JWT (statelessness, no cross-service Redis lookups).

Best of both worlds: simple revocation at the edge, fast verification internally.

The “JWT is stateless” claim — qualified

The pure stateless JWT model:

  • No server-side state.
  • Pure crypto verification.
  • Any server handles any request.

In practice, real JWT auth often has:

  • Refresh tokens in DB (for rotation + revocation).
  • Access token blocklist for emergency revocation.
  • User token-version counter (for “log out everywhere”).
  • JWKS cache (per-process state).

By the time you’ve added these for production safety, you’re more “stateless-ish with caches” than truly stateless. The pure-stateless ideal is rare.

This isn’t a knock on JWT — the practical model still scales better than per-request session lookups. Just don’t sell JWT as a silver bullet for statelessness.

XSS vs CSRF — different threats

For browser auth:

Session cookie JWT in localStorage JWT in HttpOnly cookie
XSS steals the token? no (HttpOnly) yes — usable from anywhere no (HttpOnly)
XSS uses it in-page? yes, while it runs yes yes, while it runs
CSRF (cross-site cookie send)? yes — need protection no (JS adds header) yes — need protection

The summary:

  • HttpOnly stops theft, not use: an injected script can still send requests that carry the cookie for as long as it runs.
  • Session cookies + CSRF tokens: time-tested defense.
  • JWT in localStorage: XSS takes the token away. Don’t.
  • JWT in HttpOnly cookies: equivalent security to sessions but adds JWT complexity. Storage options in full: Access and Refresh Tokens.

If you’re using JWT in HttpOnly cookies, you’ve reinvented sessions with extra steps. Reflect on whether you need JWT.

Performance comparison (rough)

For a typical API request:

Session JWT
Cookie parsing ~10 μs ~10 μs
Token lookup 200-500 μs (Redis) 100-200 μs (RSA verify)
Per-request DB 1 (session lookup) 0

For most apps the difference is noise. For very high-RPS services (50k+/sec), the absence of Redis call matters. For everyone else, premature optimization.

Migration paths

If you’re starting fresh: pick session unless you have a specific reason for JWT.

If you have JWT and want sessions:

  • Issue server-side sessions on login.
  • Tokens become opaque session IDs.
  • Add Redis or scale your DB.

If you have sessions and want JWT:

  • Add a token-issuing endpoint.
  • Refactor middleware to verify tokens.
  • Add refresh + rotation.
  • Add blocklist for revocation.

Migration in either direction takes weeks of careful work. Don’t switch without a clear win.

Common pitfalls (when picking one)

  • JWT for everything because “it’s modern” — you’ve inherited revocation complexity for no real gain.
  • Sessions across microservices without a shared store — only works on one server.
  • JWT with all the session features bolted on — token-version, blocklist, refresh DB — at that point you have sessions plus parsing overhead.
  • Mixing cookie auth and JWT auth on the same domain — confusing CSRF model.

Common interview confusions

  • “JWTs replace sessions.” — JWTs solve a specific problem (stateless verification, cross-service). For typical monolithic auth, sessions are simpler. Both coexist in modern systems.
  • “Sessions don’t scale.” — Redis handles tens of thousands of ops/sec on cheap hardware. “Doesn’t scale” is rarely a real concern below very high RPS.
  • “JWTs are more secure.” — different threat model. An HttpOnly cookie stops XSS from stealing the session, but not from using it: injected script can still make authenticated requests from the page. JWTs in localStorage add theft on top of that.

Interview angle 7

  • “JWT vs session — when each?” — session for single-domain monoliths, when revocation matters, when you can run Redis. JWT for microservices / multi-service verification, OIDC, serverless, true cross-domain SSO. Hybrid is common.
  • “What’s the main downside of JWT?” — revocation. Stateless = can’t kick someone out before exp. Mitigations (blocklist, short tokens, token-version) sacrifice statelessness. Sessions revoke trivially.
  • “How do JWTs scale better than sessions?” — no per-request session-store lookup; each server verifies locally with the public key. For very high-RPS or cross-service auth this matters; for typical apps it’s premature optimization.
  • “Can sessions work across microservices?” — yes with a shared session store (all services read from the same Redis). Adds latency per request and a SPOF. For service-to-service auth, JWT is usually cleaner.
  • “Why might storing JWT in localStorage be a bad idea?” — XSS-vulnerable. Any injected JS reads localStorage → full session compromise. HttpOnly cookies are JS-inaccessible. For SPAs: refresh token in HttpOnly cookie, access token in memory.
  • “You inherit a JWT-based app with no revocation. What do you change?” — short access token lifetime (5-15 min) immediately. Refresh token rotation + DB-backed revocation. Optionally a Redis blocklist for emergency revocation of specific tokens. Token-version in claims for “log out everywhere.”
  • “In a hybrid system, where does JWT fit?” — user-facing auth uses sessions (easy revocation, privacy). Internal service-to-service auth uses short-lived JWTs (statelessness, fast verification, no cross-service Redis lookups).