Preventing Cache Stampedes: Mastering Redis TTL and Jitter
Avoid database crashes caused by synchronized cache expiration. Learn how to implement jitter in Redis TTLs and understand the difference between passive and active deletion.
01 Feb 2026, 00:43 UTC

The Danger of Synchronized Expiration
Imagine deploying a high-traffic application where you cache a heavy database query for 60 minutes. To optimize performance, you cache 1,000 different product pages at the exact same time during a deployment or a scheduled warm-up. For an hour, your system is lightning fast. Then, at minute 61, all 1,000 keys expire simultaneously.
This creates a cache stampede. Your application suddenly finds 1,000 cache misses at once, triggering 1,000 simultaneous, expensive queries to your primary database. This spike can saturate database connections, increase latency for all users, and in worst-case scenarios, crash the backend entirely.
The solution isn't just setting a Time-To-Live (TTL)—it's introducing jitter to decouple expiration events.
How Redis Actually Handles Expiration
Before implementing a fix, it is important to understand that Redis does not use a precise timer for every single key. Instead, it employs two distinct mechanisms to reclaim memory:
- Passive Deletion: When a client attempts to access a key, Redis checks if the TTL has expired. If it has, the key is deleted and a null value is returned.
- Active Deletion: Redis periodically tests a random sample of keys with an associated TTL. If it finds expired keys, it deletes them. This prevents memory from filling up with keys that are never accessed again.
Because active deletion is probabilistic, an expired key might technically remain in memory for a short window after its TTL has passed. This means you cannot rely on expiration as a precise trigger for business-critical events.
Implementing Jitter to Smooth the Load
Jitter is the practice of adding a small, random amount of time to your TTL. Instead of every key expiring at exactly 3,600 seconds, you might set them to expire between 3,300 and 3,900 seconds. This spreads the database load over a 10-minute window rather than a single second.
Worked Example: Session Management with Jitter
In this example, we implement a 30-minute session window. To avoid a mass logout or session-refresh spike, we add a random offset of +/- 5 minutes.
# Logic implemented in application code (Pseudo-code)
# Base TTL: 1800 seconds (30 mins)
# Jitter: Random integer between -300 and 300 seconds
base_ttl = 1800
jitter = random.randint(-300, 300)
final_ttl = base_ttl + jitter
# Run this command on the Redis Primary
# SET [key] [value] EX [seconds]
# Example: SET session:user:123 "active" EX 1742
redis.set("session:user:123", "active", ex=final_ttl)
To maintain a "sliding window" (where the session extends as the user stays active), you can re-issue the EXPIRE command on each request. However, if you want to update the session data without resetting the expiration timer, use the KEEPTTL option:
# Update value but keep the original expiration timestamp
# SET [key] [new_value] KEEPTTL
redis.set("session:user:123", "updated_active", keepttl=True)
The Intersection of TTL and Eviction
A common misconception is that a TTL guarantees a key will stay in memory until it expires. This is not true if your Redis instance reaches its maxmemory limit. Depending on your maxmemory-policy, Redis may evict keys early:
| Policy | Behavior | Impact on TTL |
|---|---|---|
allkeys-lru |
Evicts Least Recently Used keys. | Keys may be deleted regardless of TTL. |
volatile-ttl |
Evicts keys with the shortest remaining TTL. | Prioritizes removing keys closest to expiration. |
noeviction |
Returns errors on write operations. | TTL is respected, but writes fail when full. |
Verification and Limitations
To verify that your TTLs are working as expected, you can use the TTL command on a specific key. This returns the remaining seconds until expiration.
Diagnostic Check:
Run the following on your Redis CLI to see how many keys are being cleared by the active expiration process:
INFO stats
Look for the expired_keys metric. If this number spikes violently in sync with your application's latency spikes, you likely have a cache stampede problem.
Limitations
- Replication: Replicas do not calculate their own TTLs. The primary sends a
DELcommand to replicas when a key expires. This ensures consistency but means the primary handles the computational load of expiration. - CPU Spikes: While jitter helps the database, an extremely high volume of simultaneous expirations can still cause CPU spikes on the Redis primary during the active deletion cycle.
Closing Action
Review your current caching logic. If you are using a static integer for your EX or PX arguments, you are vulnerable to stampedes. Introduce a random jitter of 5-10% of the total TTL to distribute your backend load and increase system resilience.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.