Redis TTL Jitter Solves Synchronization, Not Every Stampede
Guepard Team
14 novembre 2026
When thousands of keys expire together, a healthy cache can abruptly send a burst of misses to the database. Adding random variation to expiration spreads that work out. It does not prevent many concurrent requests for one popular missing key from all rebuilding it.
Treat synchronized expiry and hot-key contention as two different problems. Use TTL jitter for the first, then choose a bounded coalescing or stale-serving policy for the second.
The concrete problem
This example caches a catalog response for approximately one minute. It uses application-side single-flight conceptually: concurrent requests in one process share the same computation. Cross-process coordination requires a separate design and is not supplied by an in-process lock.
Figure: Many synchronized expiries → Jitter spreads key refreshes → Per-key coalescing limits builders.
A worked example
import random
def ttl_seconds():
return 60 + random.randint(-10, 10)
# Illustrative cache fill:
encoded = redis.get(key)
if encoded is None:
# singleflight is an application facility, not a Redis command.
with singleflight(key):
encoded = redis.get(key) # check again after becoming the builder
if encoded is None:
value = load_from_source_with_deadline()
encoded = encode(value)
redis.set(key, encoded, ex=ttl_seconds())
return decode(encoded)
Why the behavior matters
The second GET matters because another request may have filled the key while this request waited. Source reads need a deadline and a concurrency budget so a failed cache does not overwhelm the primary database. Jitter is deliberately bounded to keep the freshness contract understandable.
Stale-while-revalidate is another option: retain a value with soft and hard expiry, let one worker refresh it after soft expiry, and reject or block beyond hard expiry. That policy is appropriate only when serving older data is acceptable. Encode timestamps and version semantics explicitly rather than interpreting an arbitrary Redis TTL as business freshness.
Failure modes and design tradeoffs
A global “rebuild lock” serializes unrelated keys and can become an outage amplifier. Per-key coordination is usually a better starting point. A distributed lease must expire, release only when the owner still holds it, and account for a builder that outlives the lease. A lock by itself does not guarantee exactly one source read forever.
Do not let Redis failures silently disable every rate or load limit. The cache's failure mode should have an intentional database budget and user-facing degradation policy.
What to verify before adoption
Load-test a cold popular key, simultaneous expiry of many keys and a Redis outage. Count source calls, queue time, timeout rates and stale responses. Compare fixed TTL and jitter under the same request pattern. Report measured results only after running the experiment; this article supplies the method, not performance claims.
Primary reference
- Redis documentation. Verify the exact deployed version and driver behavior before using the example in a production runbook.
Guepard Team
Guepard Engineering