Manipulate cache size to see hit rates change in real time. Walk through write strategies step-by-step. Watch cache stampedes unfold — then see XFetch stop them cold.
A cache is only useful if most reads come from it, not the database. The difference between 90% and 99% hit rate is a 10× difference in DB load — not 9%. Feel it with the slider below.
The three strategies — cache-aside, write-through, and write-behind — differ in who writes what, and when. Step through each write flow to see the exact sequence and where things can go wrong.
| Strategy | Write Latency | Stale Data? | Cache Warm on Write? | Data Loss Risk | Best For |
|---|---|---|---|---|---|
| Cache-Aside | Low | Until TTL/miss | No (lazy fill) | None | Read-heavy, tolerate brief stale |
| Write-Through | High (2 writes) | Never | Yes | None | Consistency-critical reads |
| Write-Behind | Lowest | Never | Yes | Yes (cache crash) | Write-heavy, eventual durability OK |
A popular key expires. 500 concurrent requests all miss the cache simultaneously and all fire DB queries at once. The DB collapses. This is the thundering herd problem — watch it unfold.
Each read checks: now − delta × beta × ln(rand()) > expiry − TTL
delta = how long recomputation takes (seconds). beta = aggressiveness (1.0 = default). If the formula evaluates true, this worker refreshes proactively — even though the key hasn't expired yet. Everyone else still hits the warm cache. One refresh, zero stampede.
Distributed mutex (Redis SET NX): First miss acquires a lock, fetches DB, populates cache. All other waiters block briefly, then retry the cache.
Background async refresh: Never expire keys synchronously — always refresh asynchronously before TTL. Serve slightly stale during refresh.
Request coalescing: Deduplicate in-flight cache miss requests — only one DB query fires per key per time window, all others wait on that result.
TTL (time-based expiry) and eviction (memory-pressure removal) are separate mechanisms. TTL is predictable; eviction depends on the policy and workload. Choosing wrong costs you cache hit rate.
Keys expire after a set duration: EXPIRE key 300. Redis uses two strategies: lazy expiry (check on access) + active expiry (background scan ~10 keys/100ms). Add jitter: 300 + random(60) to prevent synchronized expiry spikes.
On memory pressure, evicts the key not accessed for the longest time. Redis uses probabilistic LRU — samples N random keys, evicts the oldest. Use allkeys-lru for general caches. Increase maxmemory-samples from 5→10 for better accuracy.
Evicts the key accessed least often. Better than LRU for bursty workloads where a popular key happens to not be accessed recently (e.g., between traffic spikes). Redis tracks a decaying frequency counter. Use allkeys-lfu.
volatile-lru: Only evicts keys with a TTL set. Keys without TTL are never evicted — use when you have both cache keys (with TTL) and permanent keys (no TTL) in the same Redis.
allkeys-lru: Evicts any key. Use when Redis is a pure cache.
Cache invalidation is hard because you need to decide when stale data becomes unacceptable. Three answers: TTL tolerance, event-driven invalidation, and versioned keys. Each has failure modes.
Accept stale data for up to TTL seconds. Works when staleness is tolerable — product listings, news feeds, recommendation scores. Not acceptable for bank balances, inventory counts, or anything with financial consequence.
DB write triggers cache delete/update via message queue or CDC (Change Data Capture). Cache is consistent after event propagates (~ms). Problem: at-least-once delivery means you may delete twice — that's fine. Losing the event means stale cache indefinitely — that's not fine.
user:42:v7 — bump the version number on every write. Old versions are simply never read again (TTL-evicted). No explicit invalidation needed. Downside: storage grows until eviction. Cache is never "warmed" — every new version is a cold start.
On a write, only write to DB — don't touch the cache. Cache fills on next read miss. Avoids polluting cache with write-once data (batch imports, log ingestion). High miss rate initially but prevents cache churn for rarely-re-read data.
Race condition: Thread A reads from DB (old value) → Thread B deletes cache → Thread A writes old value back to cache → cache is now stale.
Fix — double-delete: Delete cache → write DB → sleep 500ms → delete cache again. The second delete catches any stale write from thread A. Crude but effective in practice.
Cleaner fix: Use Redis Lua scripts for atomic read-modify-write, or optimistic locking with version fields.
maxmemory-policy volatile-lru. Cache is full. You SET a new key without an EXPIRE. What happens?