jwtauthenticationsessionsweb security

JWT vs server sessions: which one for your app (and why)

Technical comparison of stateless JWT auth versus cookie sessions: security, logout, scaling, CSRF and when each model actually fits.

August 24, 2026·8 min read

"Should I use JWT or sessions?" is the most repeated authentication question in modern development, and it's usually framed wrong: as if JWT were the new thing and sessions the old one. Reality: they're two models with opposite trade-offs, and choosing badly produces the classic bugs — the user who deleted their account but stays logged in, the stolen token that can't be revoked. Here's the honest comparison.

The session model: the server remembers

The classic flow, working for decades:

  1. User sends credentials.
  2. Server creates a session record (memory, Redis or database) with a secure random ID.
  3. That ID travels to the client in an HttpOnly cookie and returns automatically on every request.
  4. The server checks the store: if the session exists and hasn't expired, the request is valid.
Set-Cookie: sid=a8f5f167...; HttpOnly; Secure; SameSite=Lax; Path=/

The key property: state lives on the server. Logout = delete the record. Change permissions = update it. Ban = remove it. Everything takes effect immediately because every request validates against current truth.

The JWT model: the client carries its papers

A JSON Web Token packages claims (sub, role, exp...) signed by the server:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9   ← header (algorithm)
.eyJzdWIiOiIxMjM0NSIsInJvbGUiOiJhZG0i...}  ← payload (Base64url, readable)
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJVadQssw5c   ← HMAC signature

The server stores nothing: verifies the signature with its secret key (or public key, with RS256/EdDSA), checks exp, and trusts the payload. Purely stateless: any backend instance validates without querying a shared store.

The nuance surprising everyone: the payload is only Base64url-encoded. Anyone can read it — the JWT decoder shows it in one second. The signature protects against modification, never against reading. Never put sensitive data inside.

The real trade-off table

Aspect Sessions + cookie JWT
Instant revocation Trivial (delete record) Hard (see below)
Horizontal scaling Needs shared store Natural (stateless)
Client storage HttpOnly cookie (JS-inaccessible) localStorage (XSS steals it) or cookie
Size per request ~40 bytes ID Grows with payload
CSRF risk Present → mitigate with SameSite None when sent in header
Extra data Server-side only Inside token (readable)
Microservices / mobile APIs Coupled to central store Fits naturally

JWT's two structural problems

1. There is no real logout. If a token is valid until exp, deleting it client-side invalidates nothing: whoever copied it keeps using it for hours. Partial workarounds all carry cost:

  • Short lives (5-15 min): shrinks the window, doesn't close it; multiplies refreshes.
  • Token blacklist: you just rebuilt the server-side state JWT promised to avoid.
  • Key rotation: revokes EVERYONE, not one user.

2. Refresh tokens relocate the problem rather than solving it. The usual pattern — short access token + long refresh token to renew — moves the valuable asset into the refresh. And that one does usually live server-side or encrypted, returning to stateful-land through the back door. It's still a good pattern, but let's be honest: it's not "magic stateless JWT", it's sessions in disguise with extra steps.

localStorage's structural problem

If you keep JWTs in localStorage to send them in Authorization headers, a single XSS vulnerability hands every active user's tokens to the attacker — with no trace on the server. HttpOnly cookies exist precisely for this: JavaScript cannot read them even with XSS present. With cookies you need CSRF defense again (SameSite=Lax covers most cases today), a far smaller and better-solved problem than XSS.

What to choose, decided unambiguously

  • Traditional monolith web app (Next.js SSR, Django, Rails): cookie sessions. Revocation, simplicity and maturity win. You don't need JWT for this.
  • API consumed by native mobile + web + third parties: short-lived access JWT + refresh token, with refresh rotation detection (reuse = theft → revoke the family).
  • Microservices: signed JWT (ideally asymmetric, RS256/EdDSA) propagated between internal services; the gateway validates once and each service verifies locally without calling the auth center per request.
  • Sensible hybrid (what many serious products do): browser session managed via cookie + exchange for ephemeral JWT when calling other domains.

Always, under both models: mandatory HTTPS, passwords hashed with Argon2/bcrypt, login rate limiting and short expirations wherever possible. To inspect what a token actually contains —header, payload and expiry— paste it into our JWT decoder; and to understand the cryptography underneath, the encryption vs hashing vs encoding differences clarify what an HMAC signature guarantees.

FAQ

Can I put a JWT in a cookie? Yes, and it's the best of both worlds for websites: revocation via jti list in Redis (cheap since it's only checked at login/refresh) plus HttpOnly XSS protection.

RS256 or HS256? HS256 (symmetric) suffices when a single service validates. As soon as multiple services or third parties must verify without being able to sign, go asymmetric (RS256/ES256/EdDSA): public key verifies, private key stays with the issuer.

Do JWTs replace OAuth? No: OAuth 2.0 is the authorization delegation framework; JWT is often just the format of access tokens OAuth issues. They're different layers of the same system.


Decode and inspect any JWT with our online decoder, free and the token never leaves your browser.

Try it without code

JWT Decoder

Decode JWT tokens instantly.

Open JWT Decoder

Built by

Miguel Ángel Colorado Marin (MACM)

Full-Stack Developer · Guadalajara, España

I develop web apps, digital tools and full projects — from design to deployment.

Contact me