← Back to blog
Security·August 4, 2026·7 min read

JWK Rotation in Production: What Actually Breaks When You Rotate a Signing Key

Rotating a JWT signing key is a distributed cache-invalidation problem wearing a cryptography costume — here is the four-stage timeline, the kid collision that breaks it silently, and why your overlap window is set by your slowest verifier cache, not your token TTL.

JWK Rotation in Production: What Actually Breaks When You Rotate a Signing Key

The failure mode almost never shows up during the rotation itself. It shows up four hours later, when a token signed with the old key hits a service that already dropped it from its JWKS cache, and every request carrying that token starts failing with an opaque invalid_signature or kid not found error. By the time someone notices, the deploy that triggered it has scrolled off the top of the incident channel, and the on-call engineer is debugging a problem that looks like it has nothing to do with key rotation.

This is the gap between the theory of JWK rotation — "just publish a new key and phase out the old one" — and the practice, which is a distributed cache-invalidation problem wearing a cryptography costume.

What a JWK and a JWKS actually are

A JSON Web Key (JWK) is a JSON representation of a single cryptographic key — public, and sometimes private — standardized in RFC 7517. A JWK Set (JWKS) is just an array of JWKs under a keys member, typically served from a well-known endpoint like /.well-known/jwks.json. An RSA public key as a JWK looks like this:

{
  "kty": "RSA",
  "kid": "2026-08-key-1",
  "use": "sig",
  "alg": "RS256",
  "n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zqm4Cn39wQ8sTv3ryuJ_i-DBqW-c6xBUkBnwEQ0lm8h8w3zwG7BX5rTx8VoYMBaS9IN5PhqRpXQq_hqZaLQZ7ADzZ9-h6ZZ5FE43GkQXrJKmVJcQxRXNw",
  "e": "AQAB"
}

Two fields do the operational heavy lifting here. kid (key ID) is how a relying party picks the right key out of the set — the JWT header carries a matching kid, and the verifier looks it up. alg declares the signing algorithm the key is meant to be used with, which matters more than it looks (more on that below). Neither field is defined by the key's cryptographic material; both are metadata you choose when you provision the key, and that choice is what rotation tooling actually depends on.

Why kid is the whole game

Everything about safe rotation reduces to one property: a verifier must be able to select the correct key for a given token without knowing in advance which key signed it. That's what kid buys you. Without it, a JWKS with two keys is ambiguous — a verifier would have to try every key until one validates, which is slow, and worse, it quietly accepts a token as long as any key in the set matches, which is not a distinction you want blurred during a rotation window when both an old and a new key are live simultaneously.

This is also why kid collisions are the most common self-inflicted rotation incident. If a deploy script regenerates a key but reuses the old kid — because the naming scheme is a static string, or a timestamp with insufficient resolution — verifiers with a cached JWKS silently start failing every token signed with the new key, because the cached entry under that kid still has the old public key material. The fix isn't cleverness, it's discipline: derive kid from something that changes deterministically with the key, most commonly the RFC 7638 thumbprint of the key itself (a canonical hash of its required members), so the ID and the key material can never drift apart.

The rotation timeline, stage by stage

A safe rotation is a state machine with (at minimum) four states, and the entire discipline is in not skipping one:

1. Pre-publish New key added to JWKS, unused for signing old key still signs 2. Cutover Issuer signs new tokens with new key + new kid both keys in JWKS 3. Overlap Old tokens still validate against old key duration ≥ max token TTL 4. Retirement Old key removed from JWKS entirely only after overlap ends The overlap window is set by the LONGEST-LIVED token still in circulation — not by how long you'd like the rotation to take. A 24h refresh token means a 24h+ overlap, minimum.

The stage most teams get wrong is skipping straight from pre-publish to retirement — rotating the signing key and pulling the old public key from the JWKS in the same deploy. That's safe only if every outstanding token signed with the old key has already expired, which is rarely true for anything with a refresh-token lifetime measured in days.

Cache TTLs are the real adversary

Relying parties don't fetch the JWKS on every request — that would mean a network round-trip per token verification, which no one accepts. Instead, they cache it, typically for anywhere from five minutes to 24 hours depending on the library's defaults. This means the JWKS your issuer serves and the JWKS a given verifier is actually checking against can diverge for the entire cache TTL.

The practical consequence: a key must remain in the published JWKS for at least as long as the longest verifier cache TTL plus the longest token lifetime, not just the token lifetime alone. If your OIDC library caches for one hour and your access tokens live for fifteen minutes, you need at least an hour of overlap even though the token itself expires much sooner — because a verifier could fetch the JWKS right before you retire the old key, cache it for the full hour, and then need to validate a token signed moments before retirement.

Most JWKS client libraries (the jose and jwks-rsa families being the common ones in Node, similarly named equivalents in Python and Go) handle the inverse problem reasonably well: on a kid cache miss, they refetch the JWKS once before failing, which covers the "new key just got added" case. They do not generally protect you from the "old key just got removed" case — that's a cache staleness problem in the other direction, and it's on you to size the retirement delay correctly.

alg confusion is a distinct, older failure mode

Separately from timing, there's a class of vulnerability tied to what alg a verifier trusts. The classic version — a verifier accepting alg: none and treating an unsigned token as valid — was patched out of essentially every mainstream JWT library years ago and shows up now mostly in custom implementations that hand-roll verification. The more subtle, still-relevant version is the RS256/HS256 confusion attack: if a verifier is written to accept whatever alg a token header claims rather than pinning it to the algorithm it expects for that kid, an attacker can take a known RSA public key, sign a forged token with HS256 using the public key bytes as an HMAC secret, and have a naively-written verifier accept it — because the public key is, by definition, not secret. The fix is to never let the token dictate its own verification algorithm; pin alg per kid server-side, matching what the JWKS actually published for that key.

Where this shows up in DPoP and mutual-TLS

RFC 7638 thumbprints — the canonical hash of a JWK's required members, sorted and serialized with no whitespace — exist precisely so two implementations that parse the same key material produce an identical fingerprint, independent of field ordering or optional metadata like alg or use. That determinism is what DPoP and mutual-TLS-bound tokens rely on to cryptographically bind a token to a specific key without a central registry: the thumbprint is the identifier, computed the same way by every party. It's also what lets you confirm, during a rotation, that the kid you assigned actually corresponds one-to-one with the key you think it does — if your kid generation logic and the RFC 7638 thumbprint of the same key ever diverge, that's the bug, not a coincidence. A JWK thumbprint calculator is a fast way to check that by hand against a specific key during incident debugging, without trusting whatever your rotation tooling claims it computed.

The takeaway

Rotation isn't a cryptographic operation, it's a distributed-cache invalidation problem with a signature scheme attached. Get the kid derivation deterministic (thumbprint-based, not a counter or timestamp you might collide), size the overlap window off the slowest verifier cache and the longest-lived token — not off how fast you'd like the rotation to go — and never let a token's own header dictate the algorithm it's checked against. Skip any of those three and the rotation will "work" in staging, where nothing has a cache, and fail in production four hours after the deploy that nobody's watching anymore.

#jwk#jwks#key-rotation#oauth#jwt#security

Related reading

Security
Why Your Email Regex Is a Denial-of-Service Waiting to Happen
Security
JWT vs Session Cookies: The Trade Isn't Statelessness, It's Revocation
Security
bcrypt vs scrypt vs Argon2: How Password Hashing Actually Differs