The Core Problem: Where Does Auth State Live?
Every authentication system has to answer one question: where does the server remember that a user is logged in?Sessions and JWTs answer this question differently — and that difference ripples through security, scalability, and logout behavior.
Sessions: State on the Server
The server stores session data in a database or memory. The client gets a session ID in a cookie. Every request, the server looks up the ID to find the user.
JWT: State in the Token
All user data is encoded inside a signed token. The client stores the token and sends it with every request. The server verifies the signature — no database lookup needed.
How Session Authentication Works
The session flow has four steps:
1. User logs in with credentials
POST /login → { email, password }
2. Server creates a session record in the database
sessions table: { id: "sess_abc123", userId: 42, expiresAt: ... }
3. Server sets a cookie with the session ID
Set-Cookie: sessionId=sess_abc123; HttpOnly; Secure; SameSite=Lax
4. Every subsequent request:
Browser sends cookie automatically
→ Server looks up "sess_abc123" in database
→ Finds userId=42, user is authenticatedLogout is simple: delete the session record. The session ID in the cookie becomes worthless instantly.
How JWT Authentication Works
The JWT flow eliminates the database lookup:
1. User logs in with credentials
POST /login → { email, password }
2. Server creates a signed JWT containing user data
Header: { "alg": "HS256", "typ": "JWT" }
Payload: { "sub": "42", "email": "[email protected]", "exp": 1751234567 }
Signature: HMACSHA256(header + "." + payload, SECRET_KEY)
3. Server returns the token to the client
{ "token": "eyJhbGci...SflKxwRJ" }
4. Client stores token (memory, localStorage, or cookie)
Every request includes: Authorization: Bearer eyJhbGci...
5. Server verifies the signature — no database lookup needed
If valid → user is authenticated
If expired or tampered → rejectWant to inspect a real JWT? Paste it into our JWT Debugger to see the decoded header, payload, and expiration.
Side-by-Side Comparison
| Aspect | Sessions | JWT |
|---|---|---|
| State location | Server (database / Redis) | Client (inside the token) |
| Server lookup per request | Yes — database query | No — signature verification only |
| Logout | Instant — delete session record | Difficult — token valid until expiry |
| Horizontal scaling | Needs shared session store | Stateless — any server can verify |
| Token revocation | Trivial | Requires blacklist or short expiry |
| XSS risk | Low (HttpOnly cookie) | High if stored in localStorage |
| CSRF risk | Yes (cookie-based) | No (Authorization header) |
| Mobile / API clients | Awkward (cookies) | Natural (Authorization header) |
| Cross-domain / SSO | Hard | Easy (same token works anywhere) |
| Payload size | Tiny (just the session ID) | Larger (all claims in token) |
The Logout Problem with JWTs
This is the most underestimated problem with JWTs. Once a token is issued, it remains valid until it expires — even if the user logs out, changes their password, or their account is disabled.
Common scenario:
A user logs out at 2pm. Their JWT expires at 4pm. If an attacker had stolen that token, they still have 2 hours of valid access — even after logout.
The three practical workarounds:
- 1.Short expiry + refresh tokens — Access tokens expire in 5–15 minutes. A long-lived refresh token (stored in HttpOnly cookie, backed by a database) issues new access tokens. Logout revokes the refresh token.
- 2.Server-side token blacklist — Store revoked token IDs in Redis. Check on every request. Technically works but reintroduces server state — defeating the main JWT advantage.
- 3.Accept the trade-off — For low-risk APIs, 15-minute expiry is acceptable. Use longer expiry only for non-sensitive resources.
When to Use Sessions
- Traditional server-rendered web apps — Next.js SSR, Rails, Django, Laravel — server controls everything anyway.
- When you need immediate revocation — Banking, admin panels, high-security apps where logout must be instant and absolute.
- Single-domain apps — Your frontend and API are on the same domain — cookies work perfectly.
- Simpler security model — Session state is on the server, which is easier to audit and reason about.
- You already have a database — Adding a sessions table is trivial compared to implementing refresh token rotation.
When to Use JWT
- APIs for mobile clients — Mobile apps cannot use cookies easily — Authorization header is the natural choice.
- Cross-domain or multi-service — API on api.example.com, frontend on app.example.com — cookies require CORS setup; JWT just works.
- Microservices / service-to-service auth — Service A verifies service B's JWT without a central auth database call.
- Single Sign-On (SSO) — One JWT issued by your identity provider is accepted by multiple services.
- Stateless horizontal scaling — Any server in your fleet can verify the JWT — no shared session store needed.
The Hybrid Approach (What Most Modern Apps Use)
The best production architecture often uses both:
Access token (JWT): - Short-lived: 15 minutes - Stateless: any server can verify - Stored in memory (not localStorage) Refresh token (opaque, session-like): - Long-lived: 30 days - Stored server-side in database (can be revoked) - Stored in HttpOnly cookie (XSS-safe) Flow: Login → get access token + refresh token Request → use access token (no DB lookup) Expiry → use refresh token to get new access token Logout → delete refresh token from DB (instant revocation)
This is the approach used by Auth0, Clerk, Stytch, and most modern auth libraries. You get JWT scalability with session-like revocation.
FAQ
Which is more secure — JWT or sessions?
Neither is inherently more secure; both can be implemented securely or insecurely. Sessions with HttpOnly cookies have better XSS protection. JWTs have no CSRF risk when used in headers. The security of either depends on implementation details: token storage location, expiry times, and transport security.
Can I use JWTs for session management?
You can, but it adds complexity without much benefit for simple web apps. JWTs are most valuable when you need stateless verification across services or domains. For a standard web app where your frontend and API share the same domain, a session cookie is simpler and equally secure.
Should JWTs be stored in localStorage or cookies?
Cookies (HttpOnly, Secure, SameSite=Strict) are safer for web apps because they are inaccessible to JavaScript, protecting against XSS. localStorage is accessible to any script on your page. If you must use localStorage, ensure your CSP is strict and your app has no XSS vulnerabilities.
Key Takeaways
- Sessions store state on the server; JWTs store state in the token — this single difference drives all the trade-offs
- Sessions win on revocation; JWTs win on stateless scalability and cross-domain support
- JWTs stored in localStorage are vulnerable to XSS; use HttpOnly cookies when possible
- Immediate logout is hard with pure JWTs — use short expiry + refresh tokens or a blacklist
- Most production apps use a hybrid: short-lived JWT access tokens + server-side refresh tokens