Every token validation hit our origin. Now the JWKS lives on the edge.
To validate one of our JWTs, a service fetches two public documents from the issuer: the discovery document at /.well-known/openid-configuration, and the JWKS it points to — the set of public keys the tokens are signed with. These are the most-requested and least-secret things we serve. They are public keys, which are public by definition, and metadata that changes only when we rotate. And until recently, every one of those fetches came all the way back to our origin. Because we're multi-tenant, it was worse than that sentence makes it sound.
The origin was doing work it had no reason to do
Every tenant is its own issuer, with its own .well-known document and its own JWKS. And every relying party, every resource server, every SDK that validates a token pulls those documents — on a cold start, when its own short cache expires, once per service instance. Multiply per-tenant issuers by per-service validators by cache misses and you get a firehose of requests, all of it terminating at the application, all of it for documents that are byte-for-byte identical for every caller and change only when we rotate a key.
That is the textbook profile of something that should be cached: public, identical, rarely changing. Instead the app was rendering public keys, dynamically, on the hot path of everyone else's token validation. The keys had no business being computed per request.
Putting them on the edge
The change itself is small: set honest Cache-Control headers on the discovery and JWKS responses so Cloudflare caches them at the edge. Now a validator hits the nearest Cloudflare point of presence, and the origin serves each document roughly once per PoP per TTL instead of once per validation. Discovery is trivially cacheable — it changes almost never. The JWKS is the one you have to think hard about, because the JWKS is the one document whose entire job is to change exactly when you rotate a key, and caching a thing that must be fresh at the one moment it changes is where people hurt themselves.
How a cached JWKS bites you
Key rotation plus a cached key set is a trap, and it fails in the worst possible direction. Rotate to a new signing key, start issuing tokens with it, and a validator that fetched the JWKS before the new key appeared is holding a stale copy. A token arrives carrying the new key's id; the validator looks it up, doesn't find it, and rejects the token. The token is perfectly valid. The signature is real. The user did nothing wrong. Your own edge cache just returned a 401 to a legitimate login.
Notice which way that fails. A stale private key is a nonevent — you simply haven't started using the new one yet. A stale public key set is a self-inflicted outage, and you introduced the cache that caused it on purpose, to save origin traffic. The safety of the whole scheme lives or dies on rotation.
The rotation-safe pattern
The safety is entirely in the ordering, and the ordering is the opposite of an atomic swap. Three rules:
- Publish before you sign. The new key goes into the JWKS first, and you do not sign a single token with it until that JWKS — new key id and all — has been live at the edge for at least one full cache TTL. By the time any token bearing the new key can reach a validator, the validator's cached key set already contains the key.
- Retire after expiry, not on rotation. The old key stays published in the JWKS until the last token it ever signed has expired. Old and new coexist for the entire overlap. Pull the old key the moment you rotate and you strand every token it signed that's still valid.
- TTL shorter than the overlap. The edge cache's
max-age, plus a margin for clock skew, must be shorter than the key-overlap window. The cache is allowed to be stale; it is never allowed to be stale for longer than the window in which both keys are good. That single inequality is what makes an aggressively-cached JWKS safe.
Put together, rotation becomes a crossfade with a fixed running order: add the new key, wait a TTL for the edge and every validator to see it, start signing with it, wait for the old tokens to age out, then drop the old key. Every step is safe against a cache that's a TTL behind, because the TTL was chosen to be smaller than every window a cache could be behind.
The lesson, factored out
Caching is usually a latency decision. Caching public keys is a correctness decision, because the thing you're caching is the thing that decides which tokens are real. You can absolutely put it on the edge — you should, because it's public and it's hot — but only once rotation is expressed as an ordered overlap instead of a swap, and only once the cache TTL is provably shorter than that overlap. Get that right and the busiest, most cacheable documents in your auth system stop touching your origin, and nobody ever notices the difference, which is exactly the point.