UseToolSuite UseToolSuite

JWT vs Session Cookies: Choosing an Authentication Approach

JWT vs session cookies: stateless vs stateful auth, token revocation, CSRF and XSS risk, storage choices, and which to use for web apps and APIs.

Necmeddin Cunedioglu Necmeddin Cunedioglu 4 min read
Part of the Web Security: Encoding and Hashing Guide series

Practice what you learn

JWT Decoder

Try it free →

After a user logs in, your server needs to remember who they are on every subsequent request. JWTs and session cookies are two ways to do that. The core difference is where the “who is this user” information lives: on the server (sessions) or in the token itself (JWT).

1. How Session Cookies Work

With server-side sessions, the flow is:

  1. The user logs in.
  2. The server creates a session record (stored in memory, Redis, or a database) and gives it a random session ID.
  3. The server sends that ID back in a cookie.
  4. On each request, the browser automatically sends the cookie. The server looks up the session ID in its store to find out who the user is.

The cookie holds only an opaque ID. All the real data lives server-side. This is a stateful approach — the server keeps the state.

2. How JWTs Work

A JSON Web Token (JWT) takes the opposite approach:

  1. The user logs in.
  2. The server creates a token containing the user’s data (ID, roles, expiry) and signs it with a secret key.
  3. The token is sent to the client, which stores it and sends it on each request (often in an Authorization: Bearer header).
  4. On each request, the server verifies the signature to confirm the token is authentic and unmodified, then reads the user data straight from the token — no database lookup.

The token carries its own data, so the server doesn’t store anything. This is a stateless approach.

A JWT has three Base64URL-encoded parts — header, payload, signature:

eyJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoiYWxpY2UiLCJyb2xlIjoiYWRtaW4ifQ.<signature>
└──── header ────┘ └──────────── payload ────────────┘ └ signature ┘

Note that the payload is only encoded, not encrypted — anyone can read it. The signature doesn’t hide the data; it proves the data wasn’t tampered with. Never put secrets in a JWT payload.

3. The Central Trade-off: Revocation

This is the difference that matters most in practice.

Session cookies are easy to revoke. To log a user out everywhere, or to kill a stolen session, you delete the session record on the server. The next request with that session ID fails. You have full control because the server holds the state.

JWTs are hard to revoke. Because the server doesn’t store the token, a signed JWT is valid until it expires — even if you want to invalidate it sooner. There’s no built-in “log out everywhere” or “ban this token.” The common workarounds:

  • Short expiry + refresh tokens: make access tokens short-lived (5–15 minutes) and issue a longer-lived refresh token. To revoke, stop honoring the refresh token. Compromised access tokens still work until they expire.
  • A denylist: store revoked token IDs server-side and check every request against it. This works, but it reintroduces exactly the server-side state that JWTs were supposed to eliminate.

If your app needs reliable, immediate logout and session control, sessions handle it natively.

4. Scaling

The usual argument for JWTs is scaling. With sessions, every server needs access to the session store, so you typically run a shared store like Redis. With stateless JWTs, any server can verify a token using just the secret key — no shared store needed.

In practice this is less decisive than it sounds: a Redis lookup is fast and easy to operate, and many large systems run session stores without trouble. JWTs shine most for service-to-service auth and third-party API access, where statelessness genuinely simplifies things.

5. Security Considerations

Both can be secure; both have pitfalls.

Session cookies:

  • Set them as HttpOnly (JavaScript can’t read them, blocking XSS theft), Secure (HTTPS only), and SameSite (mitigates CSRF).
  • Because the browser sends cookies automatically, you must guard against CSRF — use SameSite=Lax or Strict, or anti-CSRF tokens.

JWTs:

  • The biggest mistake is storing them in localStorage, which any JavaScript on the page can read — one XSS bug leaks the token. Storing a JWT in an HttpOnly cookie avoids this, but then you’ve added cookie behavior (and CSRF concerns) on top of JWT’s revocation problem.
  • Validate the algorithm explicitly. The classic JWT vulnerability is accepting the alg: none token or letting an attacker switch the algorithm; always pin the expected algorithm server-side.
Session CookiesJWT
StateStateful (server stores sessions)Stateless (token holds data)
RevocationEasy (delete the session)Hard (valid until expiry)
Per-request costA session store lookupA signature verification
ScalingNeeds a shared session storeAny server can verify
Best storageHttpOnly cookieHttpOnly cookie (avoid localStorage)
Main riskCSRFXSS (if stored in JS-readable storage); no easy revocation

6. Which to Choose

  • Session cookies are the right default for a first-party web app. They’re simple, well-understood, easy to revoke, and secure with the standard cookie flags. Reach for them unless you have a reason not to.
  • JWTs fit stateless scenarios best: service-to-service authentication, third-party API access, and systems where avoiding a shared session store is genuinely valuable. Use short-lived access tokens with refresh tokens, and store them carefully.

Many systems use both: session cookies for the web frontend, and short-lived JWTs for APIs and internal services. Choose based on whether you need easy revocation and session control (sessions) or stateless verification across many services (JWT) — not on a vague sense that one is “faster.”

Necmeddin Cunedioglu
Necmeddin Cunedioglu Author
4 min read
-- views

Software developer and the creator of UseToolSuite. I write about the tools and techniques I use daily as a developer — practical guides based on real experience, not theory.