🔐

JWT vs. session cookies: which should you use for authentication?

Stateless tokens or server-side sessions? A practical comparison of JWTs and session cookies - how each works, the real trade-offs, and when to pick which.

· 8min read

Almost every web app has to answer the same question after a user logs in: how do we remember who they are on the next request? For years the default answer was a session cookie. Then JSON Web Tokens (JWTs) arrived, got wildly popular, and somewhere along the way "just use JWTs" became cargo-cult advice.

Both approaches solve the same problem - keeping a user authenticated across stateless HTTP requests - but they put the "state" in completely different places. That single difference drives every trade-off that follows. Here's how each actually works and how to choose.

How session cookies work

With a classic session, the server holds the state. When a user logs in, the server creates a session record (in memory, Redis, or a database) and hands the browser a cookie containing nothing but an opaque session ID:

Set-Cookie: sid=8f3b1a...; HttpOnly; Secure; SameSite=Lax

On every subsequent request the browser sends that cookie back automatically. The server looks up the ID, finds the session, and knows who's calling. The cookie itself is meaningless if stolen out of context - all the real data lives server-side.

How JWTs work

A JWT flips this around: the token itself carries the state. After login the server builds a small JSON payload (user ID, roles, an expiry), signs it, and sends the whole thing to the client:

eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1XzQyIiwiZXhwIjoxNz...

There's no server-side session to look up. On each request the server just verifies the signature and trusts the claims inside. If you've never looked at what's actually in one, paste a token into the JWT decoder - it's just Base64URL-encoded JSON, readable by anyone, with a signature appended that only the server's key can produce.

Because the server keeps no record, JWT auth is called stateless: any server with the signing key can validate any token, with zero shared session storage.

The trade-offs, side by side

Session cookiesJWTs
Where state livesServer (Redis/DB)Inside the token
Revoke a single loginEasy - delete the sessionHard - the token is valid until it expires
Horizontal scalingNeeds shared session storeStateless, nothing to share
Payload size per requestTiny (an ID)Larger (the whole signed payload)
Reading user dataOne lookupAlready in the token
Main attack surfaceCSRFXSS / token theft
Good fitServer-rendered appsAPIs, mobile, service-to-service

The revocation problem nobody mentions upfront

This is the trade-off that bites teams later. With a session, logging someone out - or force-killing a compromised account - is one line: delete the session record and the next request fails. Instantaneous.

With a JWT there is no server record to delete. A signed token stays valid until its exp time no matter what. If a token leaks and it's valid for an hour, an attacker has an hour. The usual fixes - short-lived tokens plus a refresh flow, or a server-side denylist of revoked token IDs - work, but notice what the denylist does: it adds server-side state back. You've reinvented sessions, just more awkwardly.

You can check exactly how long a token stays live by decoding it and reading the exp claim - the JWT decoder converts it to a readable date and tells you if it's already expired.

Security: different, not "more secure"

JWTs are not inherently more secure than sessions - the risks just move.

  • Session cookies are vulnerable to CSRF (the browser attaches them automatically), which is why SameSite and CSRF tokens exist. Set HttpOnly and JavaScript can't read them.
  • JWTs are often stored in localStorage so JS can attach them to API calls - which means any XSS on your site can read and exfiltrate the token. Storing a JWT in an HttpOnly cookie instead avoids that but brings CSRF back into scope.

There's no free lunch. The right question isn't "which is safer" but "which failure mode can my app defend against more easily."

When to use which

Reach for session cookies when:

  • You're building a traditional server-rendered web app (one backend, browser clients).
  • Instant logout and revocation matter - banking, admin panels, anything sensitive.
  • You already have Redis or a database and scaling sessions isn't a real concern.

Reach for JWTs when:

  • You're securing a stateless API consumed by mobile apps or SPAs.
  • Multiple services need to validate identity without sharing a session store.
  • Requests cross service boundaries (microservices, third-party APIs) and you want a self-contained, verifiable credential.

A lot of production systems use both: a short-lived JWT access token for API calls, plus a long-lived refresh token stored in an HttpOnly cookie. You get stateless verification on the hot path and a revocable anchor for logout.

The honest summary

Session cookies keep state on the server: simple, instantly revocable, perfect for classic web apps. JWTs move state into a signed token: stateless and scalable, ideal for APIs and distributed systems, at the cost of harder revocation and bigger requests.

Pick based on your architecture, not on hype. And whichever you use, get comfortable reading your own tokens - decoding a JWT in your browser-based decoder to inspect its claims and expiry is the fastest way to understand what your auth layer is actually doing, without ever sending a live credential to someone else's server.