JWT vs Session Tokens —
Which Should You Use?
The choice between JWTs and session tokens is really a choice about where you store trust — and what you give up when you move it out of the database.
JWT vs session tokens: the core tradeoff is revocability — a session token is a random ID your server validates against a database, so you can invalidate it instantly; a JWT is a self-contained signed token your server trusts without a database lookup, which means once issued it's valid until it expires even if the user logs out.
How Each One Works
- 1 User logs in — server generates a random, opaque session ID
- 2 Server stores session data (user ID, roles, etc.) in a database or Redis
- 3 Client receives the ID in a cookie and sends it on every request
- 4 Server looks up the ID in the store on every request to get session data
- ✓ Logout = delete the record. Token is invalid immediately.
- 1 User logs in — server generates a signed JSON payload (header.payload.signature)
- 2 No server-side storage — the token itself carries all the claims
-
3
Client sends the token in an
Authorization: Bearerheader or cookie - 4 Server verifies the cryptographic signature — no database hit required
-
!
Logout has no effect. Token stays valid until
exppasses.
Head-to-Head Comparison
| Property | Session Tokens | JWTs |
|---|---|---|
| Server-side storage required | Yes (DB / Redis) | No |
| Instant revocation | Yes | No — wait for exp |
| Works across multiple servers | Needs shared store | Yes, stateless |
| Token size | Small (ID only) | Larger (full payload) |
| Logout works immediately | Yes | No |
| User data visible in token | No | Yes (Base64, not encrypted) |
| Database hit on every auth check | Yes | No |
| Horizontal scaling | Needs Redis / sticky sessions | Native — no shared state |
When JWTs Are the Right Choice
When request authentication spans multiple independent services, passing a signed JWT lets each service verify the token locally without calling back to a central auth service on every request. The stateless nature is a genuine architectural advantage here.
If your access tokens expire quickly, the inability to revoke becomes a much smaller problem. A stolen token that expires in 15 minutes has a narrow window of harm — narrow enough for most use cases.
Service accounts, CI/CD pipelines, and API integrations don't have a "logout" concept. JWTs with a reasonable expiry are a clean fit for M2M auth where no human session needs instant termination.
If your threat model doesn't require the ability to force-logout a specific user immediately (e.g. an account takeover response), JWTs's simpler operational model can be the right tradeoff.
When Session Tokens Are the Right Choice
If your app runs on one server (or a load-balanced cluster with shared Redis), sessions are simpler to reason about, simpler to debug, and simpler to secure. The JWT stateless benefit doesn't apply when you're already centralised.
Banking apps, admin dashboards, anything that handles privileged actions — these need instant revocation. A compromised admin token that remains valid for 60 minutes is not acceptable. Sessions handle this correctly by default.
With JWTs, the roles baked into the token are stale until it expires. If you promote a user to admin or revoke a permission, a JWT holder keeps their old permissions until exp. Sessions re-read from the database, so changes are reflected immediately.
Session tokens are easy to reason about: a row in a table means the session is valid; delete the row and it's gone. JWTs require understanding cryptographic verification, claim semantics, refresh token rotation, and blocklist strategies. Complexity is a security risk in itself.
The Logout Problem — The Most Misunderstood Issue With JWTs
This is where many teams get burned. If you issue a JWT with a 1-hour expiry and the user clicks "Log out", the server typically just tells the client to delete the token. The token itself is still cryptographically valid. If it was copied or stolen before logout, it continues to work for the remainder of the hour.
This isn't a bug in your code — it's the fundamental nature of stateless tokens. The three realistic solutions are:
Keep access tokens very short-lived (5–15 minutes). Pair them with a longer-lived refresh token that is rotated (replaced) on every use. Logout = delete the refresh token server-side. The access token expires soon on its own; the window of exposure is narrow.
Every JWT should include a jti (JWT ID) claim — a unique identifier for that token. On logout, add the JTI to a Redis set with a TTL matching the token's exp. On every request, check if the JTI is in the blocklist before accepting the token. This restores instant revocation but re-introduces server-side state — partially negating JWT's core advantage.
If you need instant revocation and you're not building microservices, the most pragmatic fix for the logout problem is to not use JWTs. Server-side sessions solve this correctly without extra infrastructure.
localStorage vs Cookie — Where to Store the JWT
This is one of the most debated questions in web security, and the answer is less nuanced than the debate suggests.
| Storage Location | XSS Risk | CSRF Risk | Verdict |
|---|---|---|---|
| localStorage | High — any JS can read it | None | Avoid for sensitive tokens |
| sessionStorage | High — any JS can read it | None | Same problem as localStorage |
| httpOnly cookie | None — JS cannot access it | Mitigated by SameSite | Recommended |
The recommendation is: set the JWT in an httpOnly, Secure, SameSite=Strict cookie. httpOnly means no JavaScript on the page can read the token — not your code, not a malicious injected script. SameSite=Strict means the cookie is not sent on cross-origin requests, effectively eliminating CSRF for standard web apps.
The "JWTs should go in the Authorization header" pattern is common in SPA + API architectures, but it requires the client-side code to read the token — which means it must live somewhere JavaScript can access, which means XSS can steal it. For apps where you control the client, httpOnly cookies remove that attack surface entirely.
Common Mistakes Teams Make With JWTs
JWT payloads are Base64Url-encoded, not encrypted. Anyone who holds the token — or intercepts it — can read every claim without the signing key. Never put passwords, payment data, internal system details, or data you wouldn't show the user in a JWT payload.
A JWT with a 24-hour or 7-day expiry that cannot be revoked is a serious security liability. The industry standard is access tokens under 15 minutes paired with refresh tokens. A stolen long-lived access token is an open door until it expires.
Refresh tokens should be single-use. When a client exchanges a refresh token for a new access token, the old refresh token should be invalidated and a new one issued. If you detect a refresh token being used twice, it may indicate a stolen token — you can revoke the entire family.
This is rarer but catastrophic when it happens. Some libraries historically had a vulnerability where passing alg: none in the header would bypass signature verification. Always explicitly specify the expected algorithm when verifying — never accept whatever algorithm the token claims to use.
Decode and Inspect Your JWTs
Use the free JWT Decoder to inspect headers, payload claims, and expiry times — entirely in your browser, with no data sent anywhere.
Open Free JWT Decoder →FAQ
Can you revoke a JWT before it expires? +
Not directly — that's the fundamental limitation of JWTs. Because the server verifies the signature rather than looking up a record, a valid JWT stays valid until its exp claim passes. The common workarounds are: keep expiry very short (under 15 minutes) and use refresh tokens, or maintain a token blocklist (a Redis set of revoked JTI values) that the server checks on every request. The second option re-introduces server-side state, which partially negates JWT's stateless advantage.
Where should I store a JWT — localStorage or a cookie? +
An httpOnly, Secure, SameSite=Strict cookie is the safer choice for most web apps. localStorage is accessible to any JavaScript on the page, making it vulnerable to XSS attacks. httpOnly cookies cannot be read by JavaScript at all. The tradeoff is CSRF risk, but SameSite=Strict effectively eliminates cross-site request forgery for the vast majority of use cases.
What is the difference between JWT and OAuth? +
OAuth is an authorisation framework — it defines the flows and roles (resource owner, authorisation server, client, resource server) for delegating access. JWT is a token format — a way to encode claims as a signed JSON object. They are not alternatives to each other. OAuth commonly uses JWTs as the access token format, but OAuth can also issue opaque tokens, and JWTs can be used entirely outside OAuth flows.
How long should a JWT expiry be? +
Access token expiry of 15 minutes is a widely recommended baseline. This limits the window of exposure if a token is stolen, since it cannot be revoked. Pair short-lived access tokens with longer-lived refresh tokens (hours to days) that are rotated on each use. Avoid expiry times of hours or days on access tokens — the convenience is not worth the inability to revoke.