Skip to content

Evaluator cache stampede prevention #663

Description

@thomasmaas

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:

  1. 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.

  2. 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.

  3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    • Status
      No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions