JWT vs session cookies

A session ID is 43 bytes and revocable instantly. A realistic JWT is 324 bytes and cannot be revoked at all. Everything else follows from that trade.

The measurement

session ID (32 random bytes, base64url)     43 bytes
JWT, minimal   (sub + exp)                 131 bytes
JWT, realistic (sub, name, email, roles,
                iat, exp, iss, aud)        324 bytes

This value travels on every request. At 324 bytes the token is roughly seven times a session ID, and claims accumulate — add a permissions array and it grows again. Header size limits, commonly around 8KB, stop being theoretical once several services each add their own claims.

The difference that actually decides

A session ID is an opaque handle. It means nothing until your server looks it up, which is usually described as a drawback — a database round trip per request — but is also the entire mechanism by which you can say no. Delete the record and the session ends on the next request.

A JWT is valid because the signature verifies and the expiry has not passed. Neither of those consults your server, so there is no point at which you can intervene. A user changes their password, an employee is terminated, a token leaks — the token keeps working until it expires.

The common answer is a blacklist of revoked tokens. Look closely at what that does: every request now performs a lookup against server state, which is precisely the property JWTs were adopted to avoid. You have rebuilt sessions with extra steps and a larger cookie. The honest mitigation is to keep access tokens short-lived so the window is small.

Side by side

Session cookieJWT
Size per request~43 bytes131–324+ bytes
Revoke immediately✅ delete the record❌ wait for expiry
Verifier needs your DB✅ yes❌ no — the point
Contents readable by holder❌ opaque✅ base64url, no key needed
Works across trust domains
Failure modesstore availabilityalg confusion, none, key rotation

The "readable by holder" row is worth restating: a signed JWT is not encrypted, so every claim is visible to anyone with the token. Our decoder reads them with no key at all. Nothing sensitive belongs in a payload.

When a JWT is genuinely right

When the verifier cannot reach your session store. An API gateway deciding whether to route a request, a service in a different trust domain, a third party you issue credentials to, a mobile client talking to several backends — in each case a shared session database is either impossible or exactly the coupling you are removing. Single sign-on protocols such as OpenID Connect rely on signed tokens for the same reason.

Inside one application talking to one database, none of that holds. The session store you were avoiding is the database you are already querying.

The pattern that gets both

Split the credential. A short-lived access token — minutes, not days — carries ordinary requests and is never checked against a store, preserving statelessness. A longer-lived refresh token lives server-side and is exchanged for a new access token when one expires; that exchange does consult your database, which is where revocation becomes possible again.

Logging someone out deletes the refresh token, and the outstanding access token expires on its own within minutes. Rotating the refresh token on every exchange adds a detection property: if an old one is presented again, a copy has leaked. See JWT expiration for the time claims this depends on, and how to verify a JWT for the checks that make any of it meaningful.

Frequently asked questions

Can a JWT be revoked?

Not on its own, and this is the difference that decides most architectures. A JWT is valid because its signature verifies and its expiry has not passed; nothing about that consults your server, so there is no place to say no. A session ID is the opposite — it means nothing until the server looks it up, so deleting the record ends the session instantly. The usual workaround is a blacklist of revoked tokens, but checking it means a lookup on every request, which is exactly the statelessness JWTs were adopted for. Keeping access tokens short-lived is the honest mitigation, because it bounds the damage window rather than pretending the problem is solved.

Are JWTs bigger than session cookies?

Substantially, and it is paid on every request. We measured a random 32-byte session ID at 43 bytes once base64url-encoded, against 131 bytes for a minimal JWT carrying only a subject and an expiry, and 324 bytes for a realistic one with name, email, roles, issuer and audience. That is roughly seven times larger for the realistic case. Since the credential travels in a header or cookie on every single request, the cost is bandwidth and header-size budget — and header limits, commonly around 8KB, become a genuine ceiling once a token accumulates claims. Several services each adding their own is how projects reach that limit unexpectedly, usually in production rather than in testing where the token is minimal.

Are JWTs more secure than sessions?

No, they solve a different problem. A JWT proves that claims were issued by whoever holds the signing key and have not been altered since; a session ID proves nothing by itself but is meaningless to anyone who cannot query your store. Neither is inherently safer, and JWTs introduce failure modes sessions do not have: algorithm confusion, alg set to none, keys that cannot be rotated without invalidating everything, and the inability to revoke. Sessions have their own operational cost, mainly the store itself. Choose on whether you can afford to look up state, not on a security ranking. In most single-application systems the store you are avoiding is the database you already query.

When is a JWT actually the right choice?

When the verifier cannot reach your session store. That is the case JWTs were designed for: an API gateway checking a token before routing, a service in another trust domain, a third party you issue credentials to, or a mobile client talking to several backends. In all of these, a shared session database is either impossible or the very coupling you are trying to remove. Single sign-on protocols such as OpenID Connect use signed tokens for the same reason. Inside one application talking to one database, none of that applies, and a session is simpler to reason about as well as to operate, and simplicity is worth more than it looks in an authentication path.

What is the refresh token pattern?

It splits the credential in two so you get both properties. A short-lived access token — minutes, not days — carries ordinary requests and is never checked against a store, keeping the stateless benefit. A longer-lived refresh token is stored server-side, exchanged for a new access token when one expires, and can be revoked because the exchange does consult your database. Logging someone out deletes the refresh token; the outstanding access token dies on its own within minutes. Rotating the refresh token on each exchange adds detection: if an old one is presented again, a copy has leaked, and you can invalidate the whole family rather than guessing which side was compromised.

References

Inspect a token's size and claims with the JWT decoder. More tools at withuse.io/tools.