JWT vs Session Token
The JWT vs session token debate is fundamentally about where state lives. Session tokens (server-side sessions): the token itself is a random opaque string (e.g., a UUID or a cryptographically secure random value). The server stores the actual session data (user ID, permissions, expiry) in a database or cache (Redis, Memcached). On every request the server looks up the token in the store to retrieve session state. Revocation is instant – delete the session from the store and all future requests with that token are immediately rejected. JWT tokens: the token itself carries all claims. The server verifies the cryptographic signature and reads the claims from the payload without any database lookup. This enables stateless horizontal scaling – any server instance can verify any token. The tradeoff: since the server does not store JWTs, revocation requires a token denylist (which reintroduces database lookups for every request) or waiting for the token to expire. The 'stateless JWT' myth: many teams choose JWTs specifically to avoid storing tokens, then add a denylist to support logout, and end up with the database lookup they were trying to avoid – without the simplicity of traditional sessions.
JWTs are self-contained, cryptographically signed tokens that carry all claims the server needs without a database lookup. Session tokens are opaque random strings that the server maps to session state in a database or cache. JWTs scale horizontally with no shared state; session tokens enable immediate revocation. The right choice depends on whether revocability or stateless scalability matters more for your use case.
| Feature | JWT | Session Token |
|---|---|---|
| Token format | Signed JWT: header.payload.sig (Base64URL) | Opaque random string (UUID or random bytes) |
| State location | Client carries all state in the token | Server stores state; token is just a lookup key |
| Server lookup | None – signature verified locally | Database/cache lookup on every request |
| Revocation | Hard – requires denylist or short expiry | Instant – delete session from store |
| Horizontal scaling | Easy – any server verifies any token | Requires shared session store (Redis/DB) across servers |
| Token size | Typically 300–600 bytes | Typically 32–64 bytes |
| Transport | Authorization: Bearer header or cookie | Cookie (HttpOnly, Secure, SameSite) or header |
| Claim visibility | Payload visible to anyone who decodes it | Opaque – no data exposed in token itself |
| Expiry enforcement | Enforced by exp claim in token | Enforced by TTL in session store |
| Multi-service auth | Natural – any service with the public key verifies | Requires shared session store or service-to-service token exchange |
| Standard | RFC 7519 (JWT), RFC 8725 (Best Practices) | No formal standard – implementation varies |
When to use JWT
JWTs are the right choice for: microservice architectures where multiple services must validate the same token independently, federated identity (OIDC ID Tokens, OAuth 2.0 Bearer tokens), short-lived access tokens (15–60 minutes) where waiting for expiry is acceptable, and stateless API gateway validation at the edge. Use RS256 or ES256 so verifier services never need the signing secret.
When to use Session Token
Server-side sessions are the right choice for: web applications where immediate logout matters (banking, healthcare, admin panels), single-server or shared-store deployments, long-lived sessions (days/weeks) where revocability is critical, and any scenario where you need to enumerate or audit all active sessions for a user. Sessions stored in HttpOnly+Secure+SameSite=Strict cookies are resistant to XSS token theft.
Common Mistakes
- Storing sensitive data in JWT payload – the payload is Base64URL encoded, not encrypted. Anyone who intercepts the token can decode the payload. Never put passwords, PII, or secrets in a JWT unless using JWE (JSON Web Encryption, RFC 7516).
- Using long-lived JWTs without a denylist – a 24-hour JWT cannot be revoked when a user logs out or an account is compromised. Keep access token lifetimes short (15–60 minutes) and use refresh tokens with rotation.
- Storing JWTs in localStorage – localStorage is accessible to JavaScript and vulnerable to XSS. Store JWTs in HttpOnly cookies (inaccessible to JavaScript) when possible. Use the Authorization: Bearer header pattern for SPAs that cannot use cookies.
- Choosing JWT just to avoid a database – then adding a denylist for logout support, ending up with a database lookup on every request AND more complex token handling than a simple session.
FAQ
Can I use JWTs as session tokens?
Yes, but understand the tradeoffs. A JWT stored in an HttpOnly cookie behaves similarly to a session cookie in terms of transport security. The difference is the server-side: with JWTs there is no central revocation without a denylist, whereas session tokens can be invalidated instantly.
Which is more secure: JWT or session tokens?
Neither is inherently more secure. Security depends on implementation. Session tokens in HttpOnly+Secure+SameSite cookies are resistant to XSS. JWTs are vulnerable to XSS if stored in localStorage. Both are vulnerable to CSRF if not protected. Properly implemented, both are secure.
Why do OAuth 2.0 and OIDC use JWTs?
Because OAuth 2.0 access tokens are consumed by multiple resource servers that should not share a session database. A JWT allows any resource server to independently verify the token using the issuer's public key (from JWKS). This is the key architectural reason JWTs dominate in federated auth.