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-Controledge configuration, database tuning (→ db-skills/optimizing-sql-performance), or memoizing pure functions in code.
Core rules
- 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.
- 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.
- 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)
- ✅
- 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
vin the key; old entries age out) Mixing strategies ad hoc is how stale-forever bugs are born.
- 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. - 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
- ❌
- 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.
- 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
- Identify the hot, expensive read; record baseline latency and expected hit rate.
- Classify its data (staleness tolerance) and pick the invalidation strategy from rule 4.
- Define the key schema and TTL (+ jitter); implement cache-aside with origin fallback on cache errors.
- Add hit/miss metrics per key family.
- 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.