JWT vs sessions: what actually matters

The real question isn't "stateless vs stateful." It's "can I revoke this credential in a hurry?"

· LoginWith team

Every JWT-vs-sessions debate I’ve seen eventually gets to “JWTs are stateless, that’s cool.” It is cool. It’s also wrong about 80% of the time. The right frame is different.

The real question: revocation

The core difference between JWTs and sessions isn’t where the state lives. It’s what happens when you need to end a credential’s validity right now.

Session: delete the row in your sessions table. Done. The next request from that session fails.

JWT: the token is already in the user’s browser, already valid until its exp claim. You cannot revoke it without adding state back — a denylist, a shortened expiry forcing re-issue, or an external check on every request. All of those defeat the main reason you chose JWTs.

For most SaaS applications, you need revocation. Sign-out, admin lockouts, compromise response, fired employees, password resets — each is a case where “the user should be signed out RIGHT NOW” matters. Sessions handle this by design. JWTs do not.

When “stateless” JWTs are actually stateful

Teams that adopt “stateless” JWTs usually end up adding state back for real-world requirements:

  1. Denylist for revocation — a DB table of JWT IDs that have been invalidated. Every request checks it. Congratulations, you have a session table again.

  2. Short expiry + refresh tokens — access tokens expire in 5 minutes, refresh tokens live longer. Now you have a refresh-token DB (stateful) and twice the protocol complexity.

  3. Version claims — each user has a token_version claim, incremented on password change or force-logout. Every request reads the user’s current version from the DB. Stateful in practice.

In each case, the “stateless” property was traded for something else you needed more.

When JWTs do win

There are specific scenarios where JWTs’ stateless verification pays off:

Service-to-service calls where you don’t control the downstream. Your frontend hits your API. Your API calls a microservice in another network. The microservice needs to know who the user is. Verifying a JWT against a known public key is much simpler than asking a central session service.

Federated identity. OIDC flows hand you a signed ID token from the IdP (a JWT). Verifying it is faster and more portable than querying the IdP per request.

Offline or delayed verification. Devices that operate offline (IoT, partial network environments) can still verify a JWT without a round-trip.

The practical recommendation

For typical SaaS:

  • Session cookie, backed by DB or Redis. Set HttpOnly; Secure; SameSite=Lax. Store session ID only; look up on each request. Supports revocation, role changes, impersonation, everything.

  • Short-lived JWT access tokens for SPA → API calls if you have specific performance reasons (avoiding DB lookups on every request). Accompanied by a session-bound refresh mechanism.

  • JWTs for service-to-service where the architecture demands it.

Start with sessions. Add JWTs to specific architectural corners where they earn their complexity. Don’t default to JWTs because a blog said stateless was cool.

Want auth that just works?

Get started with LoginWith