The Core Difference
When a user logs in, your server needs a way to remember who they are on the next request. Session tokens store session data server-side; the client receives a random opaque ID. JWTs encode all data inside the token itself, signed with a secret key — the server verifies without any database lookup.
When Sessions Win
- Instant revocation — Delete the session record and the user is logged out everywhere immediately. With JWTs, you cannot revoke a token before it expires without a denylist.
- Smaller payload — A session ID is 32-64 bytes. A JWT with user data is 500-1000 bytes, adding overhead to every request.
- Simpler mental model — The server controls the session. No confusion about stale data inside a long-lived token.
Use sessions when: you need to log users out immediately (after a password change or account suspension), you have a single-server deployment or shared database, or you are building a traditional server-rendered web app.
When JWTs Win
- Microservices — Service A can verify a JWT from Service B without calling a central auth server because the signature is self-verifying.
- Stateless APIs — Mobile apps and SPAs use short-lived JWTs (15-minute expiry) with refresh tokens to stay logged in without a session store.
- Cross-domain authentication — JWTs passed in headers work across multiple domains more easily than cookies.
The Hybrid Pattern
Many production systems use both: a short-lived JWT (15 minutes) for API authorization, plus a long-lived refresh token in an httpOnly cookie. When the JWT expires, the client exchanges the refresh token for a new JWT — stateless verification for most requests while allowing revocation via the refresh token.
Common Mistakes
- Storing JWTs in localStorage — use httpOnly cookies to prevent XSS theft
- Using long JWT expiry (days or weeks) without a refresh token strategy — a stolen JWT is valid until it expires
- Putting sensitive data in JWT payload — the payload is base64 encoded, not encrypted; anyone can read it
- Not validating the algorithm header — always specify the expected algorithm explicitly to prevent algorithm confusion attacks