Skip to content
Backend & systems

Redis Cache-Aside and Cache Stampede: Design the Miss Path

When a popular key expires, Redis slows down, or an update races with an old fill, a cache can amplify database load. Design these paths before optimizing hit ratio.

By Published

5 min readEditorial analysisUpdated
  • Redis
  • Cache-aside
  • Cache stampede
  • Consistency
What to remember

Coalesce expensive fills, bound waiting and database fallback, and define acceptable staleness. A fill lock reduces duplicate work but does not make cache-aside strongly consistent.

Start with one product and an expiration

The database remains the authority for product 42. Read catalog:product:42; on a miss, read the database, cache the result and return it. These redis-cli commands illustrate a fill and invalidation on local Redis. Application code must serialize values and distinguish a missing product from a database failure.

Set the value and expiry together with SET EX. A separate SET followed by EXPIRE can leave a permanent key if the client stops between commands. Choose a TTL from tolerated staleness and refill cost; 60 seconds is a teaching value. Include tenant and representation identity in keys when they change which data a caller may see.

redis-cli SET 'catalog:product:42' '{"version":11,"price":25}' EX 60
redis-cli GET 'catalog:product:42'
redis-cli TTL 'catalog:product:42'
redis-cli DEL 'catalog:product:42'

Worked miss: 200 readers arrive together

Suppose the key expires while 200 concurrent requests need it. Each can start the same expensive query, overloading the database when the cache offers no protection. The number describes a hypothetical exercise, not measured traffic.

Coalesce requests within each process first. If duplicate fills across replicas remain costly, coordinate across replicas. TTL jitter spreads different keys' expiry times but does not coordinate readers of one expired hot key. Consider early refresh when demand and capacity are predictable.

Match the mitigation to the failure
ProblemUseful controlLimit
One hot key expiresSingleflight or fill lockBound wait time
Many keys expireTTL jitterNot per-key coordination
Redis unavailableBounded fallbackProtect the database
Brief stale data allowedStale while refreshingExplicit freshness limit

Recheck after acquiring the fill lock

A reader can miss, wait and acquire a lock after another reader fills the cache. Rechecking avoids duplicate work. Give the database read a deadline, release ownership in cleanup, and ensure waiting readers finish. This is application pseudocode, not a complete client.

Stale-while-refreshing needs a payload retained past a soft freshness deadline, with a separate hard expiry. An expired, deleted copy cannot serve stale data. Decide where staleness is acceptable; inventory, permissions and payments may require authoritative reads.

read product cache
if hit: return product
acquire short fill lock with a unique owner token
if acquired:
  read product cache again
  if still missing:
    read database with a deadline
    cache result with a jittered TTL
  release lock only if still owned
  return product
otherwise:
  briefly wait with jitter, then recheck cache
  use allowed stale data, bounded database fallback, or fail

Release only the lock you still own

SET NX PX acquires a short lease when the lock key is absent: OK means acquired; a nil response means not acquired. A network timeout leaves ownership uncertain: resolve it before proceeding as owner. Use a fresh unpredictable token per acquisition. The literal below is only for manual demonstration.

If a slow worker outlives its lease, another worker may acquire the key. An unconditional DEL from the first worker would delete the new owner's lock. The Lua comparison and deletion run together and avoid that mistake. They do not prevent duplicate work after lease expiry, nor provide a correctness lock across every failover scenario. Keep this mechanism an optimization for idempotent reads.

redis-cli SET 'lock:catalog:product:42' 'demo-owner-token' NX PX 2000
redis-cli EVAL 'if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) else return 0 end' 1 'lock:catalog:product:42' 'demo-owner-token'

A successful invalidation can still be followed by stale data

Consider this timeline: reader A misses and reads version 11; writer B commits version 12 and deletes the cache key; A then stores version 11. Updating the database before invalidating is a useful baseline, but this race still exists. A fill lock that writers never participate in does not fix it.

For bounded staleness, a short TTL may be sufficient. Stronger requirements need an explicit protocol, such as coordinating readers and writers, rejecting fills against an advanced generation, or bypassing the cache for consistency-sensitive reads. A version field by itself does not prevent stale writes. Reliable invalidation delivery also needs retry or outbox design when the database commit succeeds but Redis deletion fails.

Measure the paths that overload the database

Track misses, concurrent fills, lock waiting, database latency and stale-response age. A high hit ratio can conceal one expensive key. Use short Redis timeouts and cap database fallback concurrency so a cache outage does not become a database outage.

Test expiry, slow fills, failed invalidation and cache outages against your freshness contract. Verify that waiting requests finish and database work stays bounded. Use those outcomes to explain broader Redis interview questions.

Quick answers

Frequently asked questions

Does TTL jitter prevent every cache stampede?

It spreads expiry across different keys. Concurrent readers of one missing hot key still need coalescing, a fill lease or an appropriate stale-response policy.

Does a Redis fill lock guarantee consistency?

No. Lease expiry can allow duplicate fills, failover affects lock assumptions, and uncoordinated writers can race with readers. Use it to reduce work, then design freshness separately.

Should I delete the cache before updating the database?

Usually commit the database update first, then invalidate. Deleting first allows a reader to refill from the old database value. Even the usual order needs a policy for concurrent stale refills and failed invalidation.

Can all requests fall back to the database when Redis fails?

Only if capacity permits. Bound fallback concurrency and waiting, then choose controlled failure or permitted stale data when the budget is exhausted.

Source notes

References and review policy

Information checked on October 4, 2026. Section links identify sources for factual claims and technical explanations. Interpretations, practice scenarios and preparation recommendations are RecallDeck’s editorial work.

From reading to recall

Practice the full interview loop.

RecallDeck schedules the concepts you miss and keeps coding, design, and behavioral fundamentals available when the interviewer changes direction.

Start studying

Keep going