Skip to content
C Codeloom
Web

JWT vs Session Authentication

Compare JSON Web Tokens with classic server-side sessions across storage, revocation, scaling, and security so you can choose the right model for your app.

·4 min read · By Codeloom
Intermediate 9 min read

What you'll learn

  • How sessions and JWTs actually work
  • Where to store each safely
  • How revocation differs
  • When stateless tokens earn their keep
  • Common security mistakes

Prerequisites

  • Familiar with HTTP

What and Why

Two patterns dominate web auth: server-side sessions and JSON Web Tokens. The internet has a tribal argument over which is “better,” but the right answer is almost always “it depends on your shape of system.”

A session is a server-held record. The client only carries an opaque ID. A JWT is a signed, self-describing token; the server can validate it without a lookup. That difference, statefulness, drives almost every other tradeoff.

Mental Model

Sessions: client holds a key, server holds the lock and the secret. Logging out throws away the lock.

JWTs: client holds a sealed envelope. Anyone with the public key can verify it. Server holds nothing. Logging out is harder because the envelope is still valid until it expires.

Sessions:
Client -> cookie: sid=abc -> Server
                               |
                               v lookup sid in store
                               |
                            user record

JWTs:
Client -> Authorization: Bearer eyJ... -> Server
                                            |
                                            v verify signature
                                            |
                                         claims (sub, exp, ...)
Session vs JWT request flow

Sessions trade memory and a database hop for instant revocation. JWTs trade revocation difficulty for stateless verification at any service.

Hands-on Example

Server-side session, cookie-based:

Set-Cookie: sid=AbC123...; HttpOnly; Secure; SameSite=Lax; Max-Age=2592000

On each request the server reads the cookie, fetches sessions[sid] from Redis or a database, attaches the user. Logout deletes the row. Done.

A JWT in Authorization header:

Authorization: Bearer eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiI0MiIsImV4cCI6MTcyMH0.sig

Decoded payload:

{
  "sub": "42",
  "exp": 1720000000,
  "scope": "user:read user:write"
}

Any service with the public key can verify and trust the claims. A common pattern combines both: short-lived JWT access tokens (15 minutes) and a long-lived refresh token kept in an HttpOnly cookie. Access tokens stay stateless and quick; the refresh token is a session in disguise that can be revoked centrally.

Storage matters:

  • Sessions: HttpOnly cookie. Stops XSS-based theft.
  • JWT access tokens: in-memory in the JS app, or HttpOnly cookie. Never localStorage if you can avoid it.
  • Refresh tokens: always HttpOnly, Secure, SameSite=Lax cookie.

Common Pitfalls

JWTs that never expire. A 1-year token is a 1-year vulnerability. Use short expirations and refresh.

Trying to revoke JWTs by maintaining a blacklist. Now you have state, but distributed. You have invented sessions with extra steps.

Storing JWTs in localStorage. Convenient, but any XSS exfiltrates the token instantly.

Hand-rolling JWT verification. Use a battle-tested library. Algorithm-confusion attacks (none, HS256 vs RS256 mixups) have hit major projects.

Forgetting CSRF with cookie-based auth. Cookies are sent automatically. Use SameSite=Lax and a CSRF token for non-GET requests.

Putting sensitive data in JWT claims. They are base64-encoded, not encrypted. Anyone who has the token can read it.

Skipping rotation. Refresh tokens should rotate on use. A stolen refresh token used twice should trip an alarm and revoke the family.

Practical Tips

  • For monolithic apps with a single backend: sessions are simpler, faster to revoke, and easier to reason about.
  • For microservices or multiple backends without a shared session store: JWTs (or signed access tokens) shine because verification is cheap and stateless.
  • Combine both: stateless access tokens for fast verification, stateful refresh tokens for revocation control.
  • Keep access tokens short (5 to 15 minutes). The shorter they are, the less revocation matters.
  • Sign with asymmetric keys (RS256, EdDSA) so consumer services only need the public key. This avoids spreading shared secrets.
  • Include a jti (token ID) so you can selectively revoke if needed.
  • Track sessions per device in your store. Users want a “log out everywhere” button.
  • Log auth decisions. When something is wrong, you will want to see exactly which token was accepted or rejected.

Wrap-up

Sessions are not legacy. JWTs are not magic. They solve different problems.

If you can afford a session lookup and have a single backend, sessions are usually the cleaner default. If your traffic crosses service boundaries or runs at edge scale, JWTs earn their complexity. Most mature systems land on a hybrid: short JWTs guarded by a server-controlled refresh flow.

Pick the model that matches your operational reality, store tokens correctly, and revisit the decision when your architecture changes.