backend / authentication / jwt / 07_jwt_vs_session.md

JWT vs Session-Based Auth

7 interview angles 6 min read source

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: ../../07_rest_apis/09_stateful_vs_stateless.md.

The mechanics, side by side

Session-based

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

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:

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 reads token? no (HttpOnly) yes — full session leak no
CSRF (cross-site cookie send)? yes — need protection no (JS adds header) yes — need protection
Network sniffing TLS protects TLS protects TLS protects

The summary:

  • Session cookies + CSRF tokens: time-tested defense.
  • JWT in localStorage: vulnerable to XSS. Don’t.
  • JWT in HttpOnly cookies: equivalent security to sessions but adds JWT complexity.

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. Cookie sessions defend XSS well (HttpOnly); JWTs in localStorage are XSS-vulnerable.

Interview angle

  • “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).