Problem
Authorino's evaluator cache uses a fixed TTL with synchronous expiration. When multiple cache entries expire simultaneously under high concurrency, all requests miss the cache at once and flood the upstream identity/metadata sources (e.g., a database-backed API key validation service). This "thundering herd" or "cache stampede" pattern causes:
- Sudden load spikes on upstream services at every TTL boundary
- Upstream timeout failures when the burst exceeds the service's capacity
- Cascading failures when the wasm auth-service timeout (200ms default) is shorter than the upstream's burst response time
Observed behavior
Performance testing at scale (up to 512 concurrent requests, single shared API key, TTL=60s on METADATA and AUTHZ evaluators) showed:
- 93-100% cache hit rate during steady state — cache works well when warm
- 27 simultaneous cache misses out of 768 requests when the TTL expired at the 91s mark
- All 27 requests hit the upstream service concurrently instead of 1 request revalidating while others serve stale
- Three latency tiers during stampede: 1-4ms (idle upstream), ~105ms (internal queueing), ~149ms (full contention)
Proposed solutions
One or more of these strategies would mitigate the stampede:
-
Probabilistic early expiration (PER): Randomly expire a fraction of requests slightly before the TTL, spreading revalidation over time instead of a single instant. This is the simplest approach and doesn't require coordination between goroutines.
-
Single-flight / mutex revalidation: When a cache entry expires, allow only one request to perform the revalidation while other concurrent requests continue serving the stale cached value. Go's singleflight package is designed for exactly this pattern.
-
Staggered / jittered TTL: Add per-entry random jitter to the TTL at cache write time (e.g., TTL = 60s ± 10s) so entries don't all expire at the same wall-clock time.
Additional considerations
- Authorino's evaluator cache memory grows with the number of distinct cache keys (e.g., unique API keys). There is currently no configured memory limit. Consider adding a max-entries or max-memory-bytes configuration to prevent unbounded growth in production.
- The
cache.ttl field on evaluators is the only tuning knob today. More granular configuration (jitter range, max stale age) would help operators balance freshness vs. stampede risk.
References
- Performance data from PSAP team scale testing (512 concurrent, GuideLLM workload)
- Authorino evaluator cache:
pkg/evaluators/cache.go
Problem
Authorino's evaluator cache uses a fixed TTL with synchronous expiration. When multiple cache entries expire simultaneously under high concurrency, all requests miss the cache at once and flood the upstream identity/metadata sources (e.g., a database-backed API key validation service). This "thundering herd" or "cache stampede" pattern causes:
Observed behavior
Performance testing at scale (up to 512 concurrent requests, single shared API key, TTL=60s on METADATA and AUTHZ evaluators) showed:
Proposed solutions
One or more of these strategies would mitigate the stampede:
Probabilistic early expiration (PER): Randomly expire a fraction of requests slightly before the TTL, spreading revalidation over time instead of a single instant. This is the simplest approach and doesn't require coordination between goroutines.
Single-flight / mutex revalidation: When a cache entry expires, allow only one request to perform the revalidation while other concurrent requests continue serving the stale cached value. Go's
singleflightpackage is designed for exactly this pattern.Staggered / jittered TTL: Add per-entry random jitter to the TTL at cache write time (e.g., TTL = 60s ± 10s) so entries don't all expire at the same wall-clock time.
Additional considerations
cache.ttlfield on evaluators is the only tuning knob today. More granular configuration (jitter range, max stale age) would help operators balance freshness vs. stampede risk.References
pkg/evaluators/cache.go