Blog
5 min read

Session Cookies vs JWTs: Which Should Your App Use for Authentication?

Server-side sessions and JWTs both keep users logged in, with very different trade-offs. How each works, revocation and logout, where to store tokens (cookies vs localStorage), the hybrid access/refresh pattern, and a clear default for web apps.

After a user logs in, your app needs to recognise them on every following request. There are two main approaches: server-side sessions and JWTs (JSON Web Tokens). The internet is full of strong opinions on this. Here are the actual trade-offs, and a sensible default.

(Background: cookies explained and what is a JWT?)

Server-side sessions

  1. User logs in. The server creates a session record — in a database, Redis, or memory — with an ID, the user ID and an expiry.
  2. The server sends the session ID in a cookie.
  3. On each request, the browser sends the cookie; the server looks up the session ID to find the user.

The cookie holds only a random, meaningless ID. All the real information stays on the server.

JWTs

  1. User logs in. The server creates a token containing claims — user ID, roles, expiry — and signs it with a secret key.
  2. The token is sent to the client (in a cookie, or in a response body for the client to store).
  3. On each request, the client sends the token; the server verifies the signature and trusts the claims inside — no database lookup needed.

The token is the session. That's its strength and its weakness.

The trade-offs

Sessions JWTs
Where state lives Server In the token
Lookup per request Yes (fast with an index or Redis) No
Log out / revoke immediately Delete the session — done Hard: token stays valid until it expires
Change a user's role Takes effect immediately Old token keeps old role until expiry
Size sent per request Tiny ID Larger (claims + signature)
Works across many services Needs shared session store Any service with the key can verify
Complexity Low Higher to do safely

The revocation problem

This is the big one. With sessions, "log out everywhere", "ban this user" or "password changed — end all sessions" is a single delete. With JWTs, a stolen or outdated token is valid until it expires, because no server checks anything but the signature. Fixes exist — short expiry times, or a revocation list — but a revocation list is a server-side lookup again, giving up the main benefit.

The "stateless scaling" argument

JWTs are often recommended because they avoid a database lookup, which "scales better". For almost any web app, a session lookup by primary key, or in Redis, takes a fraction of a millisecond. It's rarely the bottleneck. (Horizontal vs vertical scaling.)

Where to store the token

Whichever you choose, how the browser stores it matters most:

  • HttpOnly cookie — JavaScript can't read it, so an XSS bug can't steal it. Add Secure and SameSite=Lax (or Strict). This is the recommended place for both session IDs and JWTs in web apps.
  • localStorage — any JavaScript on the page can read it, including injected scripts. A single XSS vulnerability leaks every user's token. Avoid for auth tokens.

Cookies do bring CSRF into play; SameSite cookies plus your framework's protections handle that.

The hybrid: short access tokens + refresh tokens

A common pattern, used by many auth providers:

  • a short-lived access token (a JWT valid for ~5–15 minutes) used on every request, verified without a lookup,
  • a long-lived refresh token, stored server-side and in an HttpOnly cookie, used to get new access tokens.

Revoking the refresh token cuts off the user within one access-token lifetime. It's a reasonable middle ground — and it's more moving parts, which is why using a provider or library is wise.

When each fits

Use server-side sessions when:

  • you have a typical web app with one backend (most apps),
  • you need instant logout, bans and role changes,
  • you want the simplest secure option.

Use JWTs when:

  • several independent services must verify users without sharing a database,
  • you're issuing tokens for APIs consumed by other parties or mobile apps,
  • you use an identity provider that issues them (then let its SDK handle verification).

Mistakes to avoid

  • Storing auth tokens in localStorage.
  • Long-lived JWTs (days or weeks) with no revocation.
  • Putting secrets or personal data in a JWT — the contents are only encoded, not encrypted; anyone can read them.
  • Not verifying signatures properly, or accepting the none algorithm. Use a maintained library and pin the expected algorithm.
  • Rolling your own when a framework or auth library does it correctly. (How to add login to an AI-built app.)

The summary

  • Sessions keep state on the server; JWTs carry it in a signed token.
  • Sessions make logout and revocation trivial; JWTs avoid a lookup but are hard to revoke.
  • Store either in an HttpOnly, Secure, SameSite cookie — not localStorage.
  • Default for web apps: server-side sessions. Use JWTs for multi-service or third-party API scenarios.

EasySpawn servers run Redis and PostgreSQL alongside your app, so session storage is a local, sub-millisecond lookup — and every app is served over HTTPS for Secure cookies from day one. See how it works or join the waitlist.

Related: Authentication vs Authorization · "Sign in with Google" Explained · Two-Factor Authentication · Redis: When a Small App Actually Needs It · API Authentication Methods

Keep reading