1. The Problem Both Approaches Solve
HTTP is a stateless protocol. Every request your browser sends to a server is, from the server's perspective, completely independent — the server has no built-in memory of who you are or what you did in previous requests.
This creates an obvious problem for authentication. Once you log in, how does the server know who you are on the next request? You can't re-send your password with every click — that would be insecure, slow, and impractical.
The solution is to establish a proof of identity that the client can present with each request. Session-based and JWT-based authentication are two fundamentally different strategies for creating and validating that proof. Understanding the trade-offs between them is one of the most important architectural decisions you'll make when building web applications.
2. Session-Based Authentication (Stateful)
Session-based authentication is the traditional approach, and it works by having the server remember you. When you log in, the server stores information about your session and gives you a ticket — a session ID — to present on future requests.
How it works — step by step
- Login request The client sends credentials (username + password) to the server over HTTPS.
- Server validates credentials — The server checks the credentials against the database, comparing against a stored password hash, not the plaintext password.
- Session created — The server creates a session record stored in memory, a database like Redis, or a file. This record contains the user's ID, roles, and an expiration time. The session is assigned a unique, random, hard-to-guess ID.
-
Session ID sent to client — The server sends the session ID back to the client, typically in an
HttpOnlycookie. TheHttpOnlyflag prevents JavaScript from accessing the cookie, blocking a wide class of XSS attacks. - Subsequent requests — The browser automatically includes the cookie with every request to the same domain. The server reads the session ID, looks it up in the session store, and retrieves the user's data.
- Logout — The server deletes the session record from the store. Even if someone has the session ID, it's now invalid — the record it references no longer exists.
What the server stores
The session store holds structured data. A typical session record might look like this:
{
"session_id": "a3f8c2d1e4b5...",
"user_id": 42,
"roles": ["admin"],
"created_at": "2024-01-15T09:00:00Z",
"expires_at": "2024-01-15T17:00:00Z",
"ip_address": "203.0.113.45"
}
// The client only ever sees the session ID in a cookie:
// Set-Cookie: session_id=a3f8c2d1e4b5...; HttpOnly; Secure; SameSite=Strict
The client holds nothing sensitive. The session ID is just a random string — it has no meaning on its own. All the meaningful data lives on the server.
Advantages
- Instant revocation. You can invalidate a session immediately by deleting it from the store. The next request the user makes will fail authentication. This is critical for "log out everywhere", security breaches, and account suspension.
- Server has full control. You can change a user's roles or permissions and they take effect on the very next request — no token refresh cycle required.
- Smaller attack surface on the client. Since the session ID itself reveals nothing, there's no sensitive data to extract if a client is compromised.
- Well-understood and mature. Session management libraries are battle-tested and available in every language and framework.
Limitations
- Scalability complexity. Every request must hit the session store. In a distributed system with multiple servers, all servers must share access to the same session store.
- Infrastructure dependency. The session store becomes a critical dependency. If Redis goes down, no one can authenticate.
- Memory overhead. Active sessions consume storage proportional to the number of logged-in users.
- Cross-domain challenges. Cookies are tied to a specific domain by default, making session-based auth awkward for APIs consumed by third-party clients or mobile apps.
3. JWT-Based Authentication (Stateless)
JSON Web Tokens take a fundamentally different approach: instead of the server remembering you, the server signs a document proving who you are and gives it to you. You carry it with you and present it to any server that trusts the signature.
A JWT is a compact, URL-safe string made of three Base64URL-encoded parts separated by dots:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 ← Header
.
eyJ1c2VyX2lkIjo0Miwicm9sZSI6ImFkbWluIn0 ← Payload (Claims)
.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c ← Signature
Decoded, the three parts look like this:
// Header — algorithm and token type
{ "alg": "HS256", "typ": "JWT" }
// Payload — the claims
{
"user_id": 42,
"role": "admin",
"iat": 1705312800,
"exp": 1705316400
}
// Signature — HMAC-SHA256(base64(header) + "." + base64(payload), secret_key)
// Only someone with the secret key can produce a valid signature.
How it works — step by step
- Login request — The client sends credentials to the server over HTTPS.
- Server validates credentials — Same as with sessions: check against a hashed password in the database.
- JWT created and signed — The server builds the payload, signs it with a secret key (or private key for RS256/ES256), and returns the token to the client. Nothing is stored server-side.
- Client stores the token — The client stores the JWT in an HttpOnly cookie or in memory. Storage choice has significant security implications (covered below).
-
Subsequent requests — The client sends the JWT in the
Authorization: Bearer <token>header or as a cookie. The server verifies the signature and checks the expiration claim. If both pass, the user is authenticated — no database lookup needed. -
"Logout" — The server can't invalidate the token directly, because it's stateless. The client discards the token. The token remains technically valid until its
expclaim passes. This is the core trade-off of JWTs.
Access tokens and refresh tokens
In practice, JWT systems use two tokens together:
- Access token — Short-lived (typically 5–60 minutes). Sent with every API request. Because it expires quickly, the window of risk if stolen is small.
- Refresh token — Long-lived (days or weeks). Stored securely in an HttpOnly cookie. Used only to request a new access token when the old one expires. The refresh token can be stored server-side, giving you revocation ability for that token specifically.
Advantages
- Stateless scalability. Any server instance can verify a JWT independently. No shared session store is needed.
- Cross-domain and cross-service use. JWTs are not bound to a domain like cookies. A single JWT issued by an auth server can be accepted by multiple APIs and services — ideal for microservices.
- Self-contained. The token carries its own user data. Servers can authenticate requests without hitting a database.
- Good fit for mobile and SPAs. Mobile apps and single-page applications can store and send tokens more naturally than managing cookie headers.
Limitations
- Revocation is hard. You cannot instantly invalidate a JWT. Once issued, it's valid until its expiry — unless you maintain a server-side blocklist, which reintroduces statefulness.
- Stale claims. If a user's role changes, the JWT still carries the old role until it expires. Mitigation requires short expiry times or a lookup-based approach.
- Token size. JWTs are larger than session IDs. Sending them in every request header adds overhead.
- Implementation complexity. Proper JWT handling — algorithm choice, key rotation, refresh token logic, storage — requires careful engineering. Mistakes are common and can be severe.
4. Side-by-Side Comparison
| Dimension | Session-Based | JWT-Based |
|---|---|---|
| Where state lives | Server (session store) | Client (token itself) |
| What the client stores | Opaque session ID | Signed token with encoded claims |
| Validation method | Look up session ID in store | Verify cryptographic signature |
| Database hit per request | Yes | No (unless blocklist is used) |
| Instant revocation | Yes — delete the session record | No — valid until expiry |
| Role/permission changes | Immediate effect | Takes effect only after token expires |
| Horizontal scaling | Requires shared session store | Easy — any server can verify |
| Cross-domain support | Limited (cookie domain restrictions) | Natural (token sent in headers) |
| Mobile / SPA fit | Awkward | Good |
| Token/session size | Tiny (session ID only) | Larger (header + payload + sig) |
| Infrastructure required | Session store | Only a secret/key pair |
| Logout reliability | Guaranteed | Partial (client deletes; server can't force it) |
5. Security Considerations
Where to store tokens
HttpOnly Cookie (recommended for most web apps): The token is stored in a cookie with HttpOnly, Secure, and SameSite=Strict flags. JavaScript cannot read it, eliminating XSS-based token theft. The risk shifts to CSRF attacks, mitigated by the SameSite flag and CSRF tokens where needed.
localStorage / sessionStorage (not recommended): Convenient, but any XSS vulnerability — even from a third-party script — can read localStorage and steal the token entirely. Because the attacker gets the full token, they can make requests from anywhere, not just within your app's domain.
CSRF vs XSS trade-offs
-
XSS (Cross-Site Scripting) — Malicious JavaScript executes in your app's context. Cookies with
HttpOnlyare protected;localStoragetokens are not. -
CSRF (Cross-Site Request Forgery) — A malicious site tricks your browser into making requests using your stored cookies. Mitigated by
SameSitecookie attributes and CSRF tokens. Not possible if auth is done viaAuthorizationheaders, since other sites can't set headers.
The algorithm confusion vulnerability
Early JWT libraries had a notorious vulnerability: if a server accepted the algorithm specified in the token's own header, an attacker could forge a token signed with the none algorithm. Always specify the accepted algorithm(s) explicitly in your server-side verification code — never trust the algorithm field from an incoming token.
// WRONG — trusts whatever alg is in the header
jwt.verify(token, secret)
// CORRECT — always specify the expected algorithm
jwt.verify(token, secret, { algorithms: ['HS256'] })
Short expiry times are your friend
The most practical defense against JWT revocation limitations is to use short-lived access tokens — 5 to 15 minutes for sensitive applications. Pair them with refresh tokens stored in HttpOnly cookies. If an access token is compromised, the attacker's window is small. Track refresh tokens server-side so you can invalidate them on logout or breach.
6. Common Pitfalls
Sticky sessions without a shared store — Routing all of a user's requests to the same server works until a server restarts or a new instance is added. Always use a shared session store (Redis, Memcached, a database) so any server can look up any session.
Session fixation attacks — An attacker sets a known session ID in a victim's browser before login, then waits for authentication. After login, the attacker uses the same session ID. Fix: always generate a new session ID after successful login — never reuse the pre-login session.
Missing session expiry — Sessions without an expiration time live forever. Always set a session expiry and clean up expired sessions regularly.
Putting sensitive data in the JWT payload — The payload is Base64URL-encoded, not encrypted. Anyone with the token can decode it instantly. Never include passwords, detailed PII, financial data, or security-sensitive fields.
Not validating the expiration claim — Some libraries require you to explicitly opt into expiration checking. Always verify that the current time is before the exp claim. An unexpiring JWT is effectively a permanent credential.
Using the same key for access and refresh tokens — Use separate keys, and validate the token's intended audience (aud claim) on each endpoint.
Long-lived access tokens without revocation — Issuing 24-hour or 7-day access tokens "for convenience" defeats the core benefit of short-lived tokens. Keep access tokens short (under one hour). Use refresh tokens for persistence.
Trusting the alg: none header — Never let incoming tokens choose their own verification algorithm. Always hard-code the expected algorithm in your verification logic.
7. Decision Guide: Which Should You Use?
Choose sessions when:
- You're building a traditional server-rendered web app (Rails, Django, Laravel)
- Instant account revocation is a hard requirement (banking, healthcare, enterprise SaaS)
- You're running a small-to-medium app on a single server or a few servers with a shared Redis instance
- Your users log in via a browser and stay on the same domain
- You need permission changes to take effect immediately
- Your team is more familiar with session-based patterns
Choose JWTs when:
- You're building a public API consumed by mobile apps or third-party clients
- You're running microservices where multiple services need to verify identity without a shared session store
- You're scaling horizontally to many stateless server instances
- You need cross-domain authentication (API on api.example.com, app on app.example.com)
- You're building a Single-Page Application that communicates with a separate backend API
- You're using a third-party auth provider (Auth0, Cognito, Okta) that issues tokens
Specific scenarios
Small web app (single server, browser clients): Use sessions. Simpler to implement correctly, instant revocation, no token storage concerns. A small Redis instance is perfectly sufficient.
SPA + REST API (same company, different domains): Use JWTs in HttpOnly cookies. You get cross-domain flexibility with XSS protection. Use short access tokens (15–60 min) and store refresh tokens in a separate HttpOnly cookie. Track refresh tokens server-side to enable revocation.
Mobile app (iOS / Android): Use JWTs. Store the JWT in the device's secure keychain (iOS) or Keystore (Android), not in application storage. Use short access tokens and refresh tokens.
Microservices architecture: Use JWTs with asymmetric signing (RS256 or ES256). The auth service signs with a private key; other services verify with the public key. Use short expiry times (5–15 min). If revocation is critical, maintain a small blocklist of revoked JTI values in Redis with TTL matching the token expiry.
High-security application (banking, healthcare): Use sessions, or JWTs with server-side tracking of every active token. Instant revocation is not a nice-to-have here — it's a compliance requirement.
Third-party public API: Use long-lived API keys for server-to-server integrations. Use OAuth 2.0 with JWT access tokens for user-delegated scenarios.
8. Hybrid Approaches
In production systems, the two approaches are often combined rather than used in isolation.
JWT access tokens + server-stored refresh tokens — The most common real-world pattern. Access tokens are stateless JWTs (fast to verify). Refresh tokens are stored server-side in a database table. When a user logs out or is banned, their refresh token record is deleted — they can't get a new access token. Existing access tokens expire within their short window.
Sessions for web, JWTs for API — If you serve both a web application and an API for mobile or third-party use, you can run both systems in parallel. Browser users authenticate via sessions. API clients authenticate via JWTs. Your backend validates both through separate middleware.
JWT blocklist for critical revocation — Maintain a blocklist of revoked JWT IDs (jti claims) in Redis with TTL matching each token's expiry. On each request, after verifying the signature, check whether the token's jti appears in the blocklist. The list stays small because you only store explicitly revoked tokens that haven't yet expired.
9. Conclusion
Session-based and JWT-based authentication are tools with different trade-off profiles, not a case of one being right and one being wrong. The decision comes down to three core questions:
Do you need instant revocation? If yes, sessions — or JWTs with server-side tracking — are the right choice.
Do you need stateless scalability or cross-domain token sharing? If yes, JWTs are the more natural fit.
Who are your clients? Browsers on a single domain lean toward sessions and cookies. Mobile apps and third-party API clients lean toward JWTs in headers.
For most modern applications serving both a web app and a mobile or API layer, a practical combination works well: short-lived JWT access tokens for API requests, refresh tokens stored server-side for revocation capability, and HttpOnly cookies wherever the client is a browser.
Whichever approach you choose, the non-negotiables stay the same: always use HTTPS, use HttpOnly and Secure cookies when applicable, keep secrets and keys out of your codebase, and keep token lifetimes short.
Top comments (0)