--- name: caching-strategies description: Designs application-level caching — cache-aside pattern, key naming, TTL policy, invalidation strategy, and stampede protection with Redis or in-process caches. Use when the user asks to add a cache, cache API responses or query results, speed up repeated reads, fix stale-cache or cache-invalidation bugs, or prevent cache stampedes. Do not use for HTTP/CDN edge caching configuration or for database query and index tuning. --- # Caching Strategies ## When to use / when NOT to use - **Use for:** adding or reviewing application caches (Redis, Memcached, in-process): what to cache, key design, TTLs, invalidation, failure behavior. - **Do NOT use for:** CDN/`Cache-Control` edge configuration, database tuning (→ db-skills/optimizing-sql-performance), or memoizing pure functions in code. ## Core rules 1. **Measure before caching.** Cache only reads that are demonstrably hot AND expensive; record the baseline latency and expected hit rate first. A cache below ~80% hit rate usually adds complexity for nothing. 2. **Cache-aside is the default pattern:** read cache → miss → read origin → write cache with TTL. Write-through/write-behind only when a measured write-path need exists. 3. **Every key has a TTL — no immortal keys.** TTL is the invalidation of last resort; without it, every bug becomes permanent. - ✅ `SETEX product:v1:42 300 …` - ❌ `SET product:42 …` (lives until someone remembers it exists) 4. **Choose the invalidation strategy per data class, at design time:** - tolerates staleness → **TTL-only** (pick the tolerance as the TTL) - must reflect writes → **purge/update on write** (delete the key in the write path) - broad derived data → **versioned keys** (bump `v` in the key; old entries age out) Mixing strategies ad hoc is how stale-forever bugs are born. 5. **Key schema is part of the design:** `entity:version:id[:variant]`, e.g. `product:v2:42:fr`. Every dimension that changes the value appears in the key — locale, currency, role. 6. **Never serve one user's data from a shared key.** Per-user data gets the user ID in the key; better, don't cache authorization decisions at all. - ❌ `GET profile:current` — whoever primed it wins 7. **Protect against stampedes:** jitter TTLs (±10%) so keys don't expire in sync, and use a single-flight lock so one process rebuilds a hot key while others serve slightly-stale or wait. 8. **Cache down ≠ site down.** Wrap cache calls with a short timeout (~50 ms) and fall through to origin on any cache error; a cache outage becomes a latency event, not an availability event. ## Workflow 1. Identify the hot, expensive read; record baseline latency and expected hit rate. 2. Classify its data (staleness tolerance) and pick the invalidation strategy from rule 4. 3. Define the key schema and TTL (+ jitter); implement cache-aside with origin fallback on cache errors. 4. Add hit/miss metrics per key family. 5. **Validate:** write to the origin and confirm the read path reflects it within the chosen tolerance; kill the cache and confirm requests still succeed from origin; check hit rate after a warm-up period against the target. ## Edge cases & failure modes - **Caching negative results** (not-found) — allowed with a SHORT TTL (~30 s) to absorb miss storms, but must be purged on create. - **Large values** (>100 KB in Redis) — compress or split; big values evict everything else. - **Cold start after deploy/flush** — expect an origin load spike; single-flight (rule 7) is what keeps it survivable. - **Two caches for one datum** (in-process + Redis) — layered TTLs multiply staleness; keep the in-process layer very short (~1–5 s). ## References Snippets for single-flight, jitter, and key-schema helpers: see [references/patterns.md](references/patterns.md).