The fundamental tension JWTs are designed to be verified without a database lookup — that's the entire point of a self-contained, signed token. Revocation, by definition, requires checking some external state ("has this been invalidated?"), which works against that design goal. There is no free lunch here: any revocation strategy reintroduces some amount of statefulness.
Strategy 1: Short expiration, and just wait it out The simplest strategy is to make access tokens so short-lived (1–15 minutes) that revocation is rarely needed — a compromised or logged-out token simply stops working shortly on its own. This handles the vast majority of real-world cases and pairs naturally with a revocable refresh token for the actual "log out" action.
Strategy 2: Refresh token revocation Since refresh tokens are typically opaque and database-backed (see our refresh token post), revoking a session is as simple as deleting or flagging that record. The access token issued from it will still work until it naturally expires, but no new one can be minted — bounding the damage to one token lifetime.
Strategy 3: A denylist for immediate revocation When you need a token to stop working **immediately** (a detected breach, a stolen device reported), maintain a fast lookup — Redis is the typical choice — of revoked `jti` values or user IDs, checked on every request. This reintroduces a database/cache round trip per request, undermining some of the "stateless" benefit, but it's the only way to get true immediate revocation for an already-issued access token.
- Store the
jtiorsub, plus an expiry equal to the token's remaining lifetime, so the denylist entry can be cleaned up automatically. - Check the denylist as an additional step after signature verification, not instead of it.