Your auth token is a cache
You’ve banned a user, so why do her requests still work?
Contents6 sections
A login token is a saved copy of the server’s decision to let one person in. That makes it a cache, and taking access away is cache invalidation. So the questions are who holds copies, how stale they may get, and which way they fail when the source is unreachable.
Ban a user and count her requests
Say you have to ban Mallory (security writing’s usual name for an attacker), who is scraping your API at 1 request a second. Her client holds a JWT (JSON Web Token), a signed token saying who she is and when it expires. It has 11 minutes left. Every API server checks a denylist of revoked token IDs (the token’s jti field). The list is written in us-east and copied to eu-west, where Mallory’s server runs.
Ban Mallory
Model: assumed network numbers, 1 request/sGuess first, then press Ban Mallory.
How many of Mallory’s requests get through after the ban?
- us-east denylistban written at 0 sjti entry
- eu-west region replicareceives it after the lagjti entry
- eu-west server memorythe copy each request checksjti entry
Healthy link
Mallory’s requests after the ban appear here, one tick per second.
Link cut, fail open on timeout (axis rescaled)
0 requests accepted after the ban
No ban yet. Mallory’s token has 11 minutes left.
Every server checked the denylist, but each one checked its own copy. A new entry first has to cross to eu-west, then wait for that server’s next refresh. Mallory gets in the whole time.
Now cut the link between regions. The check times out and fails open, which means it lets the request through. Nobody wants a denylist outage to become a login outage. So Mallory gets every request until her token expires, as if there were no denylist.
Every scheme caches the same decision
A JWT carries the decision inside each request. Its time to live (TTL), how long the copy counts as fresh, is its expiry minus its issue time. A session ID is only a key, and the decision lives in a session store. Revoking access is a write to the origin, the source of truth.
A 2016 blog post by joepie91 and a 2021 post by Thomas Ptacek both made this point. Each common fix is a cache pattern with a known way to fail.
| Scheme | Cache pattern | Fails when |
|---|---|---|
| Session lookup on every request | No cache | Every request pays a store read, and a network split locks everyone out |
| Short-lived token, renewed often | Short TTL, then ask the origin again | You need revocation faster than the TTL |
| Token plus a per-user “valid after” time | Version check | You need to kill 1 leaked token and keep the others |
Token plus jti denylist |
Negative cache (a cache of “no” answers) | Entries are evicted early, or lag lets a banned user through |
| Long token plus pushed revocations | Long TTL plus purge (copies deleted on demand) | A holder misses an event |
A cached decision also has 2 limits. The TTL limits when it counts, and the cache key limits where. For a token, the where is its audience, the services it was issued for (the aud field).
I learned the second limit from a response cache, not a token. In March 2026 I was working on a platform that serves several countries’ customer sites from one app. After a review finding on my own pull request, I changed how it picks the country for each request. It now trusts only values the server sets. I also added a cache-keying rule, so a response cached for one country is never served to another.
Which way does each scheme fail?
Here is the ban against 4 schemes, with fail-open checks. “Other users” means everyone else on eu-west.
| Scheme | Mallory, healthy link | Other users, link cut |
|---|---|---|
| Token only | 660 | Served |
| Token + denylist | 23 | Served |
| Session lookup | 0 | All rejected |
| Long token + push | 3 | Served |
A token with nothing to check lets her use all of it. Push delivers the ban in the model’s 3 seconds, so 3 requests get through. Session lookup wins on a healthy network but reads the store, in us-east, on every request.
Cut the link and every scheme except session lookup lets Mallory use her whole token. Session lookup stops her, but it also rejects every other eu-west user, since a session ID carries nothing to fall back on.
Only a scheme with a local copy of the claims can choose how it fails. Ptacek calls working through a network split “the primary benefit of statelessness,” and a revocation check gives it up.
Allowlists fail closed, denylists fail open
A session store is an allowlist, so a request passes only if its session is found. A denylist is the reverse, and a delay hurts each one differently. A session that hasn’t replicated yet gives a real user a 401 (“not signed in”), which is a support ticket. A ban that hasn’t replicated yet gives Mallory a 200 (success), which is an incident.
Eviction, when a full cache drops old entries, splits the same way. An evicted session logs someone out. An evicted denylist entry brings a revoked token back. So keep each entry until its token expires.
Push to the holders you can list
Microsoft’s identity service, Entra ID, can push revocation events to its own services, and then its tokens last up to 28 hours. Because it can list every holder, it uses a long TTL plus purge. So push revocations to the holders you can list, and keep TTLs short for the rest.
Choose per endpoint
You don’t need one scheme for the whole system. Most reads can live with 30 seconds of staleness, so a token plus a denylist is fine. Privileged writes and payments can’t, and they’re rare enough to afford a session lookup. Mallory’s scraper still gets its 23 reads, but her first privileged write hits the store, which already says no.