JWT vs sessions — the honest comparison.
The default advice for the last decade has been "use JWTs, sessions are old." That advice is wrong for most applications. JWTs solve specific problems — and create others. Sessions solve other problems — and have well-understood limitations. This is the working comparison: what each actually gives you, where each falls short, and the decision framework for your specific use case.
01The mental models
Sessions: the server stores authentication state. The client gets a session ID (usually in a cookie). On each request, the server looks up the session by ID to know who's authenticated.
JWTs: the server signs a token containing the authentication claims. The client stores the token. On each request, the server verifies the signature to trust the claims — no lookup needed.
Both authenticate users. The difference is where the truth lives: in the database (sessions) or in the token (JWTs).
02Where JWTs genuinely win
- Service-to-service authentication. Service A needs to call Service B. JWT lets B verify the call without round-tripping to a central auth service. Massive latency win in microservice architectures.
- Stateless authentication across domains. Single Sign-On flows, federated identity, OAuth — these are JWT's home turf.
- Short-lived tokens for specific operations. Email verification links, password reset tokens, signed download URLs. Stateless verification is genuinely useful here.
- Mobile apps without cookies. Native apps can't use cookies the same way browsers do. JWTs work cleanly as Bearer tokens.
03Where sessions still win
- Standard web applications. User logs in, browses, eventually logs out. Sessions in a Redis or database table — boring, reliable, easy.
- Anywhere you need real revocation. User changes password, user gets banned, user logs out — the session goes away immediately. JWTs can't be revoked without adding back the lookup that JWTs were supposed to eliminate.
- When tokens would carry sensitive data. JWT contents are visible to the client (just base64-encoded). Storing roles, permissions, or user data in a JWT exposes information.
- When you need fine-grained access control. Sessions can carry rich, frequently-updated state. JWTs are fixed at issue time.
04The revocation problem
This is the single biggest JWT trap. Once you issue a JWT with a 24-hour expiry, that token is valid for 24 hours. The user changes their password? The token still works. You discover the user is a fraudster? Token still works. You ban them? Token still works.
The standard workarounds:
- Short expiry + refresh tokens. JWTs expire in 5-15 minutes. Client uses a refresh token to get a new one. Revocation window is the JWT lifetime.
- Token blocklist. Maintain a database of revoked tokens. Check on every request. Congratulations, you've reinvented sessions with extra steps.
- Live a lie. Accept that revocation takes up to your JWT lifetime. Often unacceptable.
For applications where revocation must be immediate (banking, healthcare, anything with sensitive data), sessions are the right call. For applications where 5-15 minutes of delay is fine, JWTs with short expiry work.
05Where to store JWTs in the browser
This is the question that derails most JWT implementations. The options:
- localStorage: readable by any JavaScript on the page. One XSS bug and your token leaks. Common, common-ly wrong.
- sessionStorage: same XSS exposure as localStorage, plus disappears on tab close. Worst of both worlds.
- HTTP-only cookie: not readable by JavaScript. Safer against XSS. But now you have to handle CSRF, and you're using cookies anyway — at which point the original "stateless" advantage of JWTs evaporated.
- In memory only (React state): reasonably safe, but lost on every page refresh. Pair with a short-lived refresh token in HTTP-only cookie.
The honest answer: if your JWT lives in a cookie, you could have just used a session. The complexity is identical, but you've added the JWT serialization overhead and lost revocation.
06Session implementation that scales
"Sessions don't scale" was true in the 1990s. In 2026, sessions in Redis scale to millions of concurrent users without breaking a sweat.
// On login
const sessionId = crypto.randomUUID();
await redis.setex(
`session:${sessionId}`,
86400, // 24h TTL
JSON.stringify({ userId, role, createdAt: Date.now() })
);
res.cookie('sid', sessionId, {
httpOnly: true, secure: true, sameSite: 'strict'
});
// On every request
const session = await redis.get(`session:${req.cookies.sid}`);
if (!session) throw new Error('Unauthorized');
const { userId } = JSON.parse(session);
// On logout, ban, password change
await redis.del(`session:${sessionId}`);
Redis sessions handle 100K+ requests per second per node. Sharded across multiple nodes, you can handle effectively unlimited scale. The "stateless is faster" argument doesn't hold at any realistic scale.
07JWT implementation that doesn't shoot you in the foot
If you've decided JWTs are right for your case, do them correctly:
- Use RS256 or ES256. HS256 (symmetric) means anyone who can verify can also sign — too risky for distributed systems.
- Always set
expclaim. Tokens that never expire are a liability. 15 minutes for access tokens, 7-30 days for refresh tokens. - Validate the algorithm. CVE-2015-9235 (and similar): if you blindly trust the
algheader, attackers can switch tononeor forge tokens. Always specify expected algorithms. - Validate the issuer and audience. Stop tokens issued for other services from being accepted.
- Rotate signing keys. Use
kid(key ID) in headers to support graceful rotation. - Don't put secrets in the payload. Anyone with the token can read it. Roles, permissions, names: fine. Internal user IDs, billing info: not fine.
08The hybrid approach
Most production systems end up with a hybrid:
- Web sessions for user login. HTTP-only cookies, Redis-backed, immediate revocation.
- JWTs for service-to-service. Short-lived, scoped to specific operations.
- JWTs for OAuth/SSO flows. Industry standard, integrates with everything.
This isn't compromise — it's using each tool for what it's actually good at. The "all JWT" or "all sessions" purity doesn't survive contact with real production constraints.
09The decision framework
Five questions:
- Do you need immediate revocation? If yes → sessions, or JWTs with a blocklist (which negates the JWT win).
- Are you authenticating cross-service? If yes → JWTs are likely worth it.
- Are you building a standard web app? If yes → sessions are the boring, reliable choice.
- Do you need to scale to millions of concurrent users? If yes → sessions in Redis still work. So do JWTs. Both scale.
- Does your team understand JWT security pitfalls? If no → sessions are safer. JWTs have famous CVE classes; sessions are well-understood.
∞The honest take
JWTs became popular because they sound modern — stateless, decentralized, microservices-y. But "stateless" is rarely a property you actually need at the application layer, and most JWT deployments end up with state anyway (refresh tokens, blocklists, session-like infrastructure).
For a standard web application with login and logout, sessions in Redis are simpler, safer, and just as scalable. Use JWTs where they actually solve problems — cross-service auth, OAuth, short-lived signed URLs — and use sessions everywhere else. The tools are different, not opposed.