Comparison guide
JWT vs Session Tokens: How to Choose
JWT vs session tokens compared on revocation, storage, scale, and security. Concrete recommendations for monoliths, distributed apps, and short-lived APIs.
Both JWTs and session tokens let a server know that a request came from a logged-in user. The mechanics are different: a JWT carries the user's identity inside a signed blob, while a session token is an opaque random ID that the server looks up. The choice between them isn't about security — both are secure when used correctly — but about revocation, scale, and where complexity lives. This guide walks through the differences with concrete recommendations.
Decode a JWT to inspect its payload with the JWT decoder, or read on for the structural comparison.
Quick answer
Use session tokens for browser-based apps where you control the server and revocation matters. Use JWTs for service-to-service authentication, short-lived API access, and distributed systems where database lookups on every request are too expensive. The default for most products is sessions; reach for JWTs when there's a specific reason.
Comparison at a glance
| Criterion | Session token | JWT |
|---|---|---|
| Where state lives | Server (database, Redis) | Inside the token |
| Server lookup per request | Yes | Optional |
| Revocation | Immediate (delete the row) | Hard (needs allow/deny list) |
| Token size | Small (32–64 bytes) | Larger (300+ bytes signed) |
| Carries claims | No | Yes (iss, sub, exp, custom) |
| Suited to single-server | Yes | Yes |
| Suited to many-service | Awkward (shared store) | Yes (verify with public key) |
| Transport | Cookie or header | Cookie or Authorization: Bearer |
| Logout means | Delete session | Delete refresh token + wait for access expiry |
What a JWT actually is
A JWT is a base64-encoded string with three parts separated by dots:
<header>.<payload>.<signature>. The header and payload are JSON; the signature
proves the issuer wrote it.
Example payload
{
"iss": "acme.example",
"sub": "u_001",
"aud": "acme-api",
"exp": 1748390400,
"iat": 1748386800,
"scope": "read:widgets write:widgets",
"name": "Otter"
}
The server doesn't need a database to know who this is — the sub and scope
claims are right there, signed by the issuer's key. Any service that knows the
public key can verify it.
What that gets you
- No lookup on the hot path. Every request can be authorized with a signature check, which is fast and stateless.
- Cross-service trust. Service A issues, service B validates with the public key, service C does the same. No shared session store.
- Claims travel with the token. Scopes, roles, expiry — all visible without a database call.
What that costs you
- Revocation is hard. Once issued, a JWT is valid until it expires. The standard workaround is short access-token lifetimes (5–15 minutes) plus a refresh token that is stored server-side.
- Token size. A signed JWT is hundreds of bytes; sent on every request, it adds up.
- Easy to misuse. Putting sensitive data in the payload, signing with
none, accepting tokens with wrongaud, ignoringexp— every one of these has caused real production bugs.
What a session token actually is
A session token is a high-entropy random string. The server stores a row keyed by that string, containing the user ID, the issued-at time, and whatever else the application needs.
Cookie: session=8c9f2e1a-7b4d-4e3c-9a8b-1f2e3d4c5b6a
The token itself has no meaning — it's an opaque pointer. On every request the server looks up the row and reads the user state from there.
What that gets you
- Instant revocation. Delete the row; the token is dead. A user logs out, an account is suspended, a leaked token is reported — the effect is immediate.
- Mutable session data. You can update permissions or session state without re-issuing the token.
- No payload risk. Nothing sensitive is in the token, so leaking it from logs or URLs is bounded.
What that costs you
- Lookup on every request. Even a fast lookup (1 ms from Redis) adds up to seconds of CPU per million requests.
- Shared store across services. If you have multiple backend services, all of them need access to the session store — either directly or through an auth API.
- Stickiness. Some deployments need session affinity (requests for the same user routed to the same instance) which complicates load balancing.
When each one wins
Use session tokens when
- You're building a typical web app with a single backend (or a small cluster sharing one database).
- Users can log out, lose their phone, change passwords, or get suspended — all common — and you need that to take effect immediately.
- You're not sure which to pick. Sessions are the safer default.
Use JWTs when
- You're building service-to-service authentication where every service needs to verify the caller without a shared database.
- You're issuing short-lived API access tokens (OAuth-style) where the natural lifetime is minutes.
- You're operating at a scale where a session-store lookup is genuinely a bottleneck — and you've measured it, not assumed it.
- You're integrating with an external system that requires JWT (most identity providers, AWS Cognito, Auth0, Firebase, Supabase).
Don't use JWTs when
- You're storing them in
localStorage. An XSS bug becomes a full account takeover. - You think they're "more secure" without thinking about why.
- You haven't planned for revocation. "What happens when a token leaks?" is a question you need an answer to before going live.
Hybrid: short-lived access JWT + long-lived refresh token
The most common production pattern is hybrid: a short-lived JWT (5–15 minutes) for the access token, and a long-lived refresh token stored server-side. The access token gets the no-lookup benefit on the hot path; the refresh token gives back revocation. When you log out, you delete the refresh token; the access token expires within minutes and the user is fully out.
This is what every major identity provider does. If you're rolling your own auth, this is usually the right starting point.
Inspect a JWT in the browser
The free JWT decoder on JSONZen shows the header, payload, and signature status for any JWT — locally, no upload. Useful for debugging "why does this auth header not work?" or auditing what claims your provider is actually emitting.
For the payload, the JSON formatter pretty-prints it and the JSON Schema validator can check it against an OAuth or OIDC claim schema.
Closing recommendation
Pick sessions for typical web apps where revocation matters and one server (or one cluster) holds the data. Pick JWTs when authentication crosses service boundaries and a shared lookup is a real cost. Pick the hybrid model — short JWT plus refresh token — when you're issuing API access for third-party integrations. The wrong choice usually shows up six months in, when "we need to revoke a token" becomes a project.