JWTs Don't Eliminate Server State. They Relocate It.

The pitch for JWTs is always the same: stateless authentication. No session store. No database lookup on every request. Just verify a signature and read the claims. Your API servers become stateless, which means they scale horizontally without a shared session backend.

The pitch for JWTs is always the same: stateless authentication. No session store. No database lookup on every request. Just verify a signature and read the claims. Your API servers become stateless, which means they scale horizontally without a shared session backend.

All of that is true, and it's genuinely a good architecture for a lot of systems.

It's also incomplete in a way that catches teams out, usually about eighteen months in, when someone asks a question that sounds simple: "Can we log this user out immediately?"

The question that reveals the gap

A user's laptop is stolen. Security wants their access terminated now.

With server-side sessions, this is one line: delete the session record. The next request finds nothing and gets rejected.

With JWTs, there's no record to delete. The token is a self-contained, cryptographically signed statement that the user authenticated and this token is valid until its exp. Your API servers were designed specifically not to check with anyone before accepting it. That's the feature.

So the honest answer is: you can't revoke it. You can only wait for it to expire.

That answer is unacceptable in most enterprise contexts, which is why every mature JWT deployment ends up building something to work around it — and that something is state.

Where the state actually went

Here's the reframe that makes JWT architectures easier to reason about: JWTs don't eliminate state. They move it, from the access-token check to somewhere else. Once you know where to look, the tradeoff becomes visible and manageable.

Refresh tokens are state. This is the big one, and it's frequently overlooked because refresh tokens feel like an implementation detail rather than an architectural concession.

A refresh token must be revocable — that's the entire point of having one. Which means the server has to store something: which refresh tokens exist, which are still valid, which have been used, which family they belong to. If you're doing refresh token rotation with reuse detection (you should be), you're tracking token lineage across generations.

That's a stateful, mutable, per-user data structure with lifecycle management. You didn't eliminate the session store. You renamed it and made it apply less often.

flowchart LR
    subgraph Stateless["Genuinely stateless"]
        AT["Access token\nverified by signature\nno lookup"]
    end
    subgraph Stateful["State lives here"]
        RT["Refresh token store\nvalidity, rotation, families"]
        RL["Revocation list\nif you need immediacy"]
        SID["Session mapping\nfor logout propagation"]
    end
    AT -.->|"expires, must be renewed"| RT

Revocation lists are state. If short-lived tokens aren't short enough — and for admin actions or a stolen device, minutes may not be — you build a denylist. Every validator now checks whether a token ID is on it before accepting. You have reintroduced exactly the network lookup that JWTs were chosen to avoid, plus you now operate a distributed cache with correctness-critical invalidation.

Logout propagation is state. In an SSO setup with back-channel logout, an IdP notifies applications that a session has ended. To act on that, each application must map the IdP's session identifier to its own sessions. That mapping is state, and it has to be maintained from the moment of login.

Anything you need to change mid-session is state. Permissions in the token are a snapshot from issuance time. Revoke a role and the user keeps it until their token expires. Fixing that means checking permissions live — a lookup — or aggressively short lifetimes, which means more refresh traffic against the refresh token store, which is state.

Why "just use short expiry" doesn't fully resolve it

The standard answer is: make access tokens short-lived — five to fifteen minutes — and accept a bounded revocation window.

This is genuinely the right default, and it works well. But it's worth understanding what it actually trades.

Shorter access tokens mean more frequent refreshes. Each refresh is a round trip to your token endpoint, which hits the refresh token store, which is the stateful component. Halve the token lifetime and you roughly double the load on the one part of your system that isn't stateless.

At sufficient scale this inverts the original motivation. You chose JWTs so your API servers wouldn't need a shared session lookup. With five-minute tokens and a large active user base, your token endpoint is now handling substantial traffic against a database — you've concentrated the state, not removed it. Which is often still a win, because concentrated state is easier to scale and cache than state on every API server. But it's a different claim than "stateless."

The revocation window is also a real number that someone will eventually ask about. "How long after we disable an account can it still make API calls?" needs an answer, and "up to fifteen minutes" is a legitimate answer only if you've decided it is, documented it, and can defend it to a security team.

The hybrid everyone converges on

Most mature systems land in roughly the same place, and it's worth stating explicitly because teams often arrive at it by accident rather than design:

  • Short-lived JWT access tokens — local validation, no lookup, genuinely stateless in the hot path
  • Longer-lived refresh tokens, stored and revocable — the deliberate state, in one place
  • Rotation with reuse detection on refresh — a stolen refresh token that gets used twice reveals itself, and the whole family can be killed
  • A denylist for emergency revocation only — checked on the small number of paths where the window genuinely can't be tolerated, not on every request
  • Session ID (sid) in the token — so logout propagation is possible at all

The pattern: stateless where it's cheap, stateful where it's necessary, and deliberate about which is which.

What to decide explicitly

What's your revocation window, and can you say it out loud? If it's fifteen minutes, that's fine. If nobody knows, that's the problem.

Where does refresh token state live, and what happens when it's unavailable? This is now a critical dependency of your auth system. If that store is down, nobody can refresh, and everyone gets logged out as their access tokens expire. That's a slow-motion outage that looks fine on your dashboards for the first ten minutes.

Do you store sid? If not, back-channel logout is unimplementable. You'll receive a notification saying "this session ended" and have no way to know which of your sessions that refers to. Teams typically discover this when an enterprise customer requires single logout, which is the worst time.

What's in the token, and what's looked up live? Coarse, slow-changing claims — tenant, role, group — belong in the token. Fine-grained, resource-specific, frequently-changing decisions belong in a live check. Putting everything in the token gives you stale authorization and, for users with many permissions, tokens large enough to hit HTTP header size limits.

Are you rotating refresh tokens? A long-lived, static refresh token is a permanent credential. If it leaks, the attacker has standing access with no signal. Rotation plus reuse detection converts a silent compromise into a detectable event.

The point

None of this is an argument against JWTs. Local signature validation is a real benefit, and for systems with many services validating many tokens, avoiding a lookup per request is a substantial architectural win.

The argument is against the word stateless, because it leads teams to skip the design work for state they will absolutely end up needing. They ship a JWT system with no refresh token store, no revocation path, and no session mapping — and then bolt each one on under pressure, usually during a security review or an incident, in whatever shape is fastest rather than whatever shape is right.

JWTs move state from "every request" to "the boundaries" — issuance, refresh, revocation, logout. That's a genuinely good trade, and it's a much more useful mental model than statelessness, because it tells you where to look when something goes wrong.

Ask where the state went. There's always an answer.