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
- User logs in. The server creates a session record — in a database, Redis, or memory — with an ID, the user ID and an expiry.
- The server sends the session ID in a cookie.
- 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
- User logs in. The server creates a token containing claims — user ID, roles, expiry — and signs it with a secret key.
- The token is sent to the client (in a cookie, or in a response body for the client to store).
- 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
SecureandSameSite=Lax(orStrict). 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
nonealgorithm. 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
API Authentication Methods: API Keys, Sessions, JWTs, OAuth and mTLS
How should clients prove who they are to your API? API keys for server-to-server, session cookies for your own web app, bearer tokens for mobile and SPAs, OAuth for third-party access, HMAC signatures for webhooks, mTLS for service meshes. When to use each and how to do it safely.
UUID vs Auto-Increment IDs: Which Primary Key Should You Use?
Sequential integers or UUIDs for your primary keys? The real trade-offs — size, index performance, guessability, merging data, leaking business metrics — why UUIDv7 changes the answer, Postgres 18's uuidv7(), and the common hybrid of internal IDs plus public IDs.