Redis cache-aside on catalog: hit ratio, p99, invalidation — and a stampede, reproduced and fixed
catalog
How many times is "upcoming events in São Paulo, page 1" read for each time it changes?
GET key ──► hit? ── yes ─► return (no DB) │ no ─► SELECT … ─► SET key value PX ttl ─► return
catalog:event:{id}
catalog:events:city=…:page=…
If the cache absorbed 95% of reads, its outage sends 20× the load to the database.
An admin changes a price. How long can a fan see the old one, on each page?
CACHE_ENABLED=false docker compose up -d && k6 run -e RATE=300 load/cache.js CACHE_ENABLED=true docker compose up -d && docker compose exec redis redis-cli FLUSHALL k6 run -e RATE=300 load/cache.js # cold k6 run -e RATE=300 load/cache.js # warm
X-Cache
cache_hit
latency_hit
latency_miss
CACHE_TTL_LIST=3s CACHE_STAMPEDE_PROTECTION=false docker compose up -d k6 run load/stampede.js # cache_misses ≫ expirations
miss ─► SET key:lock token NX PX 2000 ├─ won ─► re-check, load, SET, release └─ lost ─► poll the cache every 25 ms (≤ 500 ms)
Outline: (1) why cache the catalog; (2) cache-aside; (3) keys and TTLs; (4) failure handling; (5) hit ratio math; (6) measuring; (7) stampede and the lock; (8) other cures.