Sessions vs JWT: Which Should You Use?

The question is not which is more modern. It is whether you can revoke access instantly, and what it costs you if you cannot.

Sessions vs JWT: Which Should You Use? — Troiana insight cover

In short

Use server-side sessions for most web applications: they are simpler, revocable instantly, and the storage cost is trivial at realistic scale. Use JWTs where you genuinely need stateless verification across services that cannot share a session store. The deciding factor is revocation — a JWT is valid until it expires, so a compromised or logged-out token keeps working unless you add the very state JWTs were meant to avoid.

How each one works

Server-side sessions. On login, the server creates a session record and gives the client an opaque identifier, usually in a cookie. Each request, the server looks up the identifier and finds who you are. The cookie means nothing on its own.

JWTs. On login, the server issues a signed token containing claims — user id, roles, expiry. The client sends it back, and the server verifies the signature without looking anything up. The token carries its own truth.

That difference — lookup versus signature — produces every trade-off below.

Revocation is the deciding factor

With sessions, revoking access is deleting a row. Log out everywhere, ban an account, respond to a compromise: instant, everywhere.

With JWTs, a valid token remains valid until it expires, because verification consults nothing. You cannot un-issue it.

That matters concretely. A user logs out on a shared computer and the token in that browser still works. You demote someone from admin and their existing token still says admin. You detect a stolen token and cannot invalidate it.

The usual mitigations each undermine the premise:

Short expiry with refresh tokens. Access tokens live minutes, refresh tokens are checked against server state. This works, and it means you now have server-side state — which is what sessions are, with more moving parts.

A revocation list. Every request checks whether the token was revoked. That is a lookup per request, which is what JWTs were supposed to avoid.

Both are legitimate. Both mean the stateless benefit was traded away.

Where each genuinely fits

Sessions suit a single application or a set of services that can share a store; anything needing immediate revocation; permissions that change during a session; and teams that would rather not think about token handling.

JWTs suit services that cannot share session state; short-lived signed grants such as a download link or a password reset; delegated authorisation across organisational boundaries, which is what OAuth uses them for; and cases where the verifying party genuinely cannot call your server.

Notice how narrow the second list is. JWTs solve a distributed-systems problem. Most applications do not have that problem, and adopt them anyway.

The mistakes that turn a JWT into a vulnerability

Storing it in localStorage. Any script on the page can read it, which makes an XSS bug into a full account takeover. Cookies with HttpOnly cannot be read by script at all. If you use tokens, put them in a secure, HttpOnly, SameSite cookie — at which point you are using cookies anyway, and could have used a session.

Trusting the alg header. Historic libraries let an attacker set the algorithm to none and skip verification. Pin the expected algorithm server-side.

Putting secrets in the payload. A JWT is signed, not encrypted. Anyone holding it can read every claim.

Long expiry to avoid refresh complexity. A 30-day access token is a 30-day window in which a stolen token works.

Believing stale claims. Roles baked in at login are a snapshot. If permissions can change, verify them against current state for anything sensitive.

The scaling argument, examined

The usual case for JWTs is avoiding a session lookup per request. Worth checking the actual numbers.

A session lookup in a cache is sub-millisecond, and a modest instance handles enormous throughput. For nearly every application, the lookup is invisible next to the database queries the request performs anyway.

The cost only becomes real at genuine scale, across services that cannot share a store. If you are not there, you are paying JWT's complexity to optimise something that was not a bottleneck — the same reasoning that applies to most premature architecture.

What actually matters either way

More incidents come from session handling than from the choice itself.

Set cookies HttpOnly, Secure, and SameSite=Lax or stricter. Rotate the session identifier on login to prevent fixation. Expire idle sessions, and cap absolute lifetime regardless of activity. Invalidate every session on password change. And give users a way to see and end their active sessions — which is both a security feature and, for many people, the first sign something is wrong.

Do those and a plain session is more secure than a carelessly implemented JWT.

The recommendation

Start with server-side sessions in a secure cookie. Simpler, revocable, hard to misuse, and adequate for the overwhelming majority of applications.

Reach for JWTs when you have a specific need they solve — cross-service verification without shared state, short-lived signed grants, delegated authorisation across boundaries.

And if you adopt JWTs and then add refresh tokens and a revocation list, that is a signal worth heeding: you have rebuilt sessions, and it may be cheaper to use them.

If you are settling this for a product with real accounts, book a call.

Common questions

Should I use JWT or sessions?

Server-side sessions for most web applications — simpler, instantly revocable, and the lookup cost is negligible at realistic scale. JWTs are for cases where services genuinely cannot share a session store, for short-lived signed grants, and for delegated authorisation across organisational boundaries.

Why can't you revoke a JWT?

Because verification consults nothing — the token carries its own signed truth, so it remains valid until it expires. A user who logs out, an account you ban, or a stolen token all keep working. The usual fixes, short expiry with refresh tokens or a revocation list, both reintroduce the server-side state JWTs were meant to avoid.

Where should I store a JWT in the browser?

In a Secure, HttpOnly, SameSite cookie — never in localStorage, where any script on the page can read it and an XSS bug becomes full account takeover. Once the token is in an HttpOnly cookie you are using cookie-based auth anyway, which is often an argument for using sessions.

Are JWTs more scalable than sessions?

Only at genuine scale across services that cannot share a store. A session lookup in a cache is sub-millisecond and invisible next to the database queries a request already performs. Below that threshold you are paying real complexity to optimise something that was never the bottleneck.

Can you put user roles in a JWT?

You can, but they are a snapshot taken at login. If permissions can change during a session — someone demoted, access revoked — the token keeps asserting the old roles until it expires. Verify permissions against current state for anything sensitive rather than trusting stale claims.

Have something worth building right?