Logout Still Worked for 30 Seconds: Token Caches on Serverless
A token verification cache meant a signed-out session kept working for up to 30 seconds. Why evicting on sign-out does not fix it on serverless, and why the answer was a small measured bound.
The problem: a request after logout still succeeded
If your API caches the result of verifying an access token, sign-out does not take effect until that cache entry expires. This is a short note on finding that in ResearchForge, a university project whose FastAPI backend verifies Supabase access tokens, and on why the fix was a number rather than a mechanism.
Each private request verifies the caller's token with Supabase Auth. One page load fires several API calls within about a second, so the backend kept a small in-memory cache of verified tokens to collapse that burst into one check. The window was 30 seconds. A live logout test against production showed what that meant: sign out, make a request with the old token, and the request succeeded. On a shared computer, "log out" has to mean it.
The comment above the constant had claimed revocation was effectively immediate. It was not, and a comment the value contradicts is worse than no comment.
Why evicting on sign-out does not fix it
The tempting fix is to delete the token from the cache when the user signs out. On a single long-running server that would work. The API runs as serverless functions, so there may be several warm instances at once, each with its own in-memory cache. Evicting on the instance that handled the sign-out leaves the others still holding the token. That fix looks complete in a code review and is racy in production.
Dropping the cache entirely would make revocation immediate, at the cost of a round trip to the auth service for every call in every page load. A shared cache would need a new piece of infrastructure for a university project's login. Neither was worth it for this.
The fix: a small, measured, documented bound
The time-to-live went from 30 seconds to 5. That still covers a page load's burst of calls, and bounds post-logout validity to roughly the time it takes to close the tab. The cache is also capped at 512 entries and cleared when full, because it is a latency optimisation and never a source of truth.
# It cannot be driven to zero without giving up the cache entirely, and it
# cannot be fixed by evicting on sign-out either: the API runs as several
# serverless instances ...
_CACHE_TTL_SECONDS = 5.0
_CACHE_MAX_ENTRIES = 512The bound is asserted in a test so it cannot quietly grow again, and it is listed as a known limitation on the project page: a revoked session stays valid for up to about five seconds.
Measuring it on the deployed system
A live script signed in, confirmed a library request returned 200, signed out, and kept sampling with the old token:
| Time after sign-out | Response |
|---|---|
| 1.6 s | 200, still accepted |
| 3.9 s | 200, still accepted |
| 6.1 s | 401, refused |
| 8.0 s to 20.7 s | 401 on every sample |
One detail from the evaluation is worth repeating: an earlier version of this live test asserted that the token was refused immediately after sign-out, and it failed. The assertion was wrong, not the system, because the system was documented to allow up to five seconds. A test should check the guarantee you actually made. The rest of the testing approach is in Testing an LLM App Without Calling a Paid API, and the account flows are in the ResearchForge case study.
The work behind this
Related
Keep reading
Why Supabase Google Sign-In Landed on localhost With a Token in the URL
Google sign-in on a custom domain returned users to localhost:3000 with an access token in the address bar. Two separate causes, the Site URL fallback and the implicit flow, and the fixes.
Testing an LLM App Without Calling a Paid API
538 tests for an LLM application, none of which call a paid model. FastAPI dependency overrides, a fake provider, generated PDFs, opt-in live tests, and where fakes stop telling the truth.
Every Policy Was Correct, and Every Policy Was Inert
A data-isolation defect that passed the entire test suite, shipped, and behaved perfectly. What it took to find it, and why the fix was to stop asking the application to be careful.