When Keyring is down
The three onUnavailable modes, and exactly what the default admits in each state of the cache.
The SDK verifies from a policy set it keeps in memory. When the control plane is unreachable, that set goes stale, and the question is what a request does then. onUnavailable answers it, globally or per route.
| Mode | A key in the cache | A key the cache has never seen |
|---|---|---|
stale-then-open (default) | Served however stale, degraded set past the budget | Allowed, degraded: true, verified: false, unless the cache can prove it does not exist |
stale-then-closed | Served however stale | 401 |
closed | 503 past maxStalenessMs | 503 |
maxStalenessMs is 60 seconds by default.
What the default admits, exactly
The cache is complete for the project, not a partial memo, so a key absent from a fresh complete snapshot is a key that does not exist, and that miss is a 401 in every mode. The fail-open branch is reached only when the store cannot prove the miss, and what it admits then is a function of the store's state:
| Store state | What a miss admits |
|---|---|
| Complete and fresh | Nothing. 401. |
Complete, stale within maxStalenessMs | Still nothing. A revocation issued inside the window is the only thing not yet honoured, on a hit. |
Complete, stale past maxStalenessMs | Any well-formed key. Measured on a rehearsal: a never-minted key answered 401 for 54.6 s, then 200. |
| Never loaded (cold start, no snapshot on disk) | Any well-formed key, for as long as the control plane stays unreachable. |
Evicted under maxCachedKeys | Any well-formed key not resident, for as long as the store stays incomplete. |
Tenant-narrowed (tenantIds) | Any well-formed key of a tenant outside the filter, permanently, by construction. |
The key format's checksum is CRC-32, a public algorithm, so producing a well-formed key costs an attacker nothing. Read the last four rows as the radius: in those states, on the default mode, an unknown key is admitted with tenant: null, no scopes and no scope check, until the next successful poll. The window is not a fixed length: it stays open for as long as the outage lasts, and closes on the first successful poll after recovery.
What to set
- Money routes:
closed. A route declaringscopesis deliberately not scope-checked on a fail-open decision, soscopesalone protects nothing there. - Internal routes that must never admit a stranger:
stale-then-closed. Known keys keep working through an outage; unknown ones do not. - Serverless, or any fresh-process-per-request platform:
stale-then-closedon the client, because every invocation starts in the "never loaded" row. - A long-lived server with mostly-read routes: the default, which is what makes an outage of the control plane invisible to your customers.
req.keyring.degraded is true on every decision that came from stale or incomplete policy, and verified is false when there was no policy record at all, so a handler can be stricter than its route.
The counters and the idempotency store
These are separate questions with separate defaults: rate limits fail open, idempotency fails closed. Rate limits and Idempotency explain the asymmetry.