Two Ways to Remember Who You Are

HTTP forgets you after every request. After you log in, the server needs a way to recognise you on the next request. The two classic approaches:

  • Sessions: the server stores your login and gives your browser a random ID in a cookie.
  • JWTs (JSON Web Tokens): the server gives you a signed token containing your identity, and doesn't store anything.

Developers argue about this constantly. The honest answer: both work, they fail in different ways, and most modern apps use a mix.


🎯 Quick Answer (30-Second Read)

  • Sessions: stateful. Easy to log someone out instantly. Needs a shared session store when you have many servers.
  • JWTs: stateless. Any server can verify them without a lookup. Hard to revoke before they expire.
  • For a typical web app: sessions in HttpOnly, Secure cookies are simpler and safer.
  • For APIs, mobile apps and microservices: short-lived JWT access tokens plus a revocable refresh token is the common pattern.
  • Don't store tokens in localStorage if you can avoid it: any XSS bug can steal them.

How Each One Works

sequenceDiagram participant B as Browser participant S as Server participant DB as Session store Note over B,DB: Session-based B->>S: Login (email + password) S->>DB: Save session {id: a91f, user: 42} S-->>B: Set-Cookie: sid=a91f (HttpOnly) B->>S: GET /orders (cookie sid=a91f) S->>DB: Look up a91f → user 42 S-->>B: Orders Note over B,DB: JWT-based B->>S: Login S-->>B: JWT {sub: 42, exp: 15 min} signed B->>S: GET /orders (Authorization: Bearer JWT) S->>S: Verify signature + expiry (no DB lookup) S-->>B: Orders

A JWT has three parts, header.payload.signature, each base64url-encoded:

{ "sub": "42", "role": "admin", "iat": 1793000000, "exp": 1793000900 }

The payload is signed, not encrypted. Anyone can read it, so never put secrets in a JWT.


Head-to-Head

Sessions JWTs
Server state Stored (DB, Redis) None needed to verify
Logout / revoke Instant: delete the session Hard: token valid until it expires
Scaling Shared session store needed Any server verifies independently
Size per request Small cookie ID Larger token on every request
Cross-service use Each service needs the store Easy, with the public key
Common risk CSRF (if cookies misconfigured) Theft from localStorage via XSS, long expiry

The Revocation Problem

Watch out: if a JWT with a 7-day expiry is stolen, or a user is banned, the token keeps working for up to 7 days unless you add a server-side check, which brings back the state JWTs were meant to avoid.

The standard fix is two tokens:

flowchart LR L["Login"] --> AT["Access token (JWT)
short: 5–15 min"] L --> RT["Refresh token
long-lived, stored server-side,
revocable"] AT -->|"expires"| R["POST /refresh"] RT --> R R -->|"still valid?"| AT2["New access token"] R -->|"revoked / banned"| X["Log out"]
  • Access tokens are verified without a lookup, so APIs stay fast.
  • Refresh tokens are checked against a store, so you can revoke them.
  • Damage from a stolen access token is limited to a few minutes.

Where to Store Tokens in the Browser

Storage XSS can steal it? CSRF risk? Verdict
HttpOnly, Secure, SameSite cookie No Low with SameSite=Lax/Strict + CSRF protection ✅ Best for web apps
localStorage / sessionStorage Yes No ⚠️ Avoid for long-lived tokens
In memory (JS variable) During the session No OK for short-lived access tokens
Set-Cookie: sid=a91f...; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=604800

Which Should You Choose?

  • Server-rendered or Next.js web app, one backend: sessions (or your auth provider's cookie-based session).
  • Public API, mobile app, many microservices: short-lived JWT access tokens + refresh tokens.
  • Using Supabase, Auth0, Clerk or similar: they typically issue JWTs and manage refresh for you. Follow their recommended cookie setup for SSR.

JWT Security Checklist

  • Use strong signing: RS256/ES256 (asymmetric) or HS256 with a long random secret
  • Reject the none algorithm and pin the expected algorithm on verify
  • Always check exp, iss and aud
  • Keep access tokens short-lived
  • Never put passwords or sensitive data in the payload
  • Rotate refresh tokens on use and revoke on logout

Frequently Asked Questions

Is JWT more secure than sessions?

Not inherently. Both are secure when implemented correctly. Sessions make instant logout and revocation easy. JWTs avoid server lookups but are harder to revoke, and they're often stored insecurely in localStorage. Storage and expiry choices matter more than the format.

Can you log out a user with JWT?

Deleting the token from the client logs the user out locally, but the token stays valid until it expires if someone copied it. Use short-lived access tokens with revocable refresh tokens, or maintain a server-side denylist for critical cases.

Should I store JWT in localStorage or cookies?

Prefer HttpOnly, Secure, SameSite cookies, which JavaScript can't read, so XSS attacks can't steal the token. localStorage is readable by any script on the page. If you use cookies, add CSRF protection, which SameSite settings largely provide.


Conclusion

Sessions keep state on the server and make logout easy. JWTs carry state in a signed token and scale without lookups, but are hard to revoke. For most web apps, cookie-based sessions are the simplest safe choice. For APIs and mobile, use short-lived JWT access tokens with revocable refresh tokens, and store tokens in HttpOnly cookies whenever you can.


Sources

Last verified: 2 November 2026.


Related reads: Password Hashing Explained · Fix Supabase RLS Policy Error · Idempotency Explained · Rate Limiter Explained

I write practical guides for developers. Follow on LinkedIn or X, or invite me to speak.