TL;DR — Redis Cluster eviction under memory pressure is one of the most common sources of latency spikes in production caches. The default
allkeys-lrupolicy, combined with aggressivemaxmemory, creates synchronous eviction cycles that block client requests. By tuning eviction policies, adjusting memory watermarks, and layering client-side strategies, you can reduce p99 eviction latency from hundreds of milliseconds to single-digit milliseconds.
The Problem: Why Eviction Creates Latency Spikes
When a Redis instance approaches its maxmemory limit, the server must free space before accepting writes. This eviction process is not free — it involves looking up keys, computing their idle time, removing them from data structures, and updating internal bookkeeping. Under the default configuration, this work happens synchronously on the main event loop.
Consider a production scenario: a Redis Cluster with 12 shards serving a recommendation engine. Each shard holds roughly 4 GB of data against a maxmemory of 5 GB. During a traffic spike, write throughput doubles, and all shards simultaneously cross the memory threshold. The result is a wave of synchronous eviction across every shard, and the p99 latency jumps from 2 ms to 180 ms.
The core issue is that eviction competes with every other operation for the same single-threaded event loop. A single volatile-lru eviction cycle can take anywhere from 0.5 ms to 50 ms depending on key size and dataset complexity. When hundreds of keys need eviction, the cumulative blocking time becomes significant.
Understanding Eviction Policies in Depth
Redis offers several eviction policies, each with different trade-offs between memory efficiency and CPU cost:
noeviction— Returns errors on writes when memory is full. Safe but useless for caching workloads.allkeys-lru— Evicts the least recently used key across all keys. Good general-purpose choice for caches.allkeys-lfu— Evicts the least frequently used key. Better for workloads with highly skewed access patterns.volatile-lru— Evicts only keys with an expiry set, using LRU. Useful when you want to preserve non-expiring data.volatile-lfu— Same as above but uses frequency-based eviction.volatile-ttl— Evicts the key with the shortest remaining TTL. Lowest CPU cost but poor memory optimization.allkeys-random/volatile-random— Random selection. Minimal CPU cost but unpredictable memory behavior.
The default allkeys-lru is a reasonable starting point, but it has a hidden cost: Redis maintains an approximate LRU cache with a default of 5 samples per eviction decision. The maxmemory-samples parameter controls this. Increasing it to 10 or 16 makes eviction decisions more accurate but increases CPU time per eviction cycle.
# Redis configuration for a cache-heavy workload
maxmemory 5gb
maxmemory-policy allkeys-lru
maxmemory-samples 10
The maxmemory-samples tuning is often overlooked. With the default of 5, Redis may evict a hot key that happens to have been accessed slightly longer ago than a cold key. With 10 samples, the approximation converges closer to true LRU at the cost of roughly 2x more comparison operations per eviction.
Architecture Patterns for Predictable Eviction
Pattern 1: Tiered Eviction with Client-Side Caching
One of the most effective strategies is to move eviction pressure off the server entirely. Redis 6+ supports client-side caching (via the CLIENT CACHING command and tracking), which allows clients to maintain a local LRU cache for read-heavy workloads. When combined with server-side eviction, this creates a tiered system where the first layer absorbs most reads.
# Pseudocode for tiered caching with Redis Cluster
class TieredCache:
def __init__(self, redis_client, local_cache_size=10000):
self.redis = redis_client
self.local = LRUCache(maxsize=local_cache_size)
def get(self, key):
# Check local cache first — zero network latency
if key in self.local:
return self.local[key]
# Fall back to Redis Cluster
value = self.redis.get(key)
if value:
self.local[key] = value
return value
def set(self, key, value, ttl=None):
self.local[key] = value
if ttl:
self.redis.setex(key, ttl, value)
else:
self.redis.set(key, value)
This pattern reduces the number of keys stored in Redis, which in turn reduces eviction pressure. In a real deployment at scale, teams have reported 40–60% reductions in eviction events by adding a local caching layer.
Pattern 2: Memory Partitioning Across Shards
Redis Cluster distributes data across 16384 hash slots. Uneven data distribution can cause some shards to hit maxmemory while others have plenty of headroom. This is especially common when keys have non-uniform sizes.
Use the CLUSTER KEYSLOT and MEMORY USAGE commands to audit distribution:
# Audit memory usage across all cluster nodes
redis-cli --cluster call <host>:<port> MEMORY USAGE <key>
redis-cli --cluster call <host>:<port> INFO memory | grep used_memory_human
If you find significant skew, consider using hash tags to force related keys onto the same slot, or redesign your key naming to distribute more evenly. The goal is to ensure that no single shard is the bottleneck for eviction.
Pattern 3: Proactive Eviction with Lazy Freeing
Starting with Redis 4.0, lazyfree-lazy-eviction enables asynchronous eviction for keys that are expensive to delete. When enabled, Redis offloads the deletion to a background thread instead of blocking the main loop.
# Enable lazy freeing for eviction, expiration, and server-side deletes
lazyfree-lazy-eviction yes
lazyfree-lazy-expire yes
lazyfree-lazy-server-del yes
repl-diskless-sync-delay-delay 5
This is particularly impactful for keys containing large data structures — sets with millions of members, sorted sets, or hashes with thousands of fields. A single large key deletion can block the event loop for 100+ ms synchronously. With lazy freeing, that same deletion happens in the background with negligible impact on foreground latency.
Tuning Strategies for Production Workloads
Strategy 1: Set maxmemory Below Physical Limits
A common mistake is setting maxmemory equal to the available RAM. This leaves no room for Redis’s own overhead, fork buffers during replication, and client output buffers. A safer approach is to set maxmemory to 70–80% of available RAM.
# Example: 16 GB RAM instance
maxmemory 12gb # 75% of available memory
This buffer prevents OOM kills and gives the eviction system time to react before the server runs out of memory entirely.
Strategy 2: Use maxmemory-policy Based on Access Pattern
Not all workloads benefit from LRU. If your data has clear access frequency patterns (e.g., a small set of hot keys accessed by most clients), allkeys-lfu will outperform allkeys-lru significantly.
The LFU implementation in Redis uses an 8-bit frequency counter per key with an aging mechanism. The counter decays over time, so recently inactive keys gradually lose their frequency score. Tuning the decay rate via lfu-log-factor and lfu-decay-time can dramatically change eviction behavior:
# Tune LFU for a workload with very skewed access
lfu-log-factor 10 # Higher = slower frequency growth (more selective)
lfu-decay-time 1 # Decay every minute (faster aging)
With lfu-log-factor at 10, a key needs 100 accesses to move from counter value 2 to 3, but 1000 accesses to move from 3 to 4. This creates a steep curve that preserves genuinely hot keys while aggressively evicting warm ones.
Strategy 3: Monitor Eviction Rate as a Primary Metric
Track evicted_keys from INFO stats as a first-class metric alongside CPU and memory. A sustained non-zero eviction rate indicates that your dataset exceeds available memory, and the eviction system is working continuously — which is a leading indicator of latency degradation.
# Monitor eviction rate in real time
redis-cli info stats | grep evicted_keys
Set an alert threshold: if evicted_keys increases by more than 100 per second for more than 30 seconds, investigate. This usually precedes observable latency spikes by minutes.
Handling the Worst Case: Cache Penetration and Stampede
When eviction removes a large volume of keys simultaneously, the next request for each evicted key must fetch from the upstream database. If many keys are evicted at once, this creates a cache stampede — a thundering herd against your backend.
Two strategies mitigate this:
- Staggered TTLs: Add a random jitter (±10%) to TTL values so keys don’t expire in clusters.
- Background refresh: Use a pattern where a background goroutine or worker proactively refreshes keys approaching their TTL, rather than waiting for a client request to trigger a cache miss.
import threading
import time
import random
class RefreshableCache:
def __init__(self, redis_client, refresh_threshold=0.8):
self.redis = redis_client
self.threshold = refresh_threshold
def get_with_refresh(self, key, fetch_fn, ttl):
value = self.redis.get(key)
ttl_remaining = self.redis.ttl(key)
if value and ttl_remaining > 0:
# Trigger background refresh if TTL is below threshold
if ttl_remaining < ttl * self.threshold:
threading.Thread(
target=self._refresh, args=(key, fetch_fn, ttl),
daemon=True
).start()
return value
# Cache miss — fetch and set
value = fetch_fn(key)
self.redis.setex(key, ttl, value)
return value
def _refresh(self, key, fetch_fn, ttl):
value = fetch_fn(key)
self.redis.setex(key, ttl, value)
This pattern prevents the synchronized expiration that leads to stampede conditions, and by keeping the cache warm during memory pressure, it reduces the eviction rate itself.
Key Takeaways
- Eviction is synchronous by default — every eviction cycle blocks the main event loop, so reducing eviction frequency is the primary lever for latency stability.
- Tune
maxmemory-samplesandlfu-log-factorbased on your access pattern — the defaults are conservative and rarely optimal for production workloads. - Enable lazy freeing (
lazyfree-lazy-eviction yes) immediately if you have any keys larger than a few KB — the latency improvement is dramatic. - Set
maxmemoryto 70–80% of RAM, not 100%, to leave headroom for Redis overhead and prevent OOM kills. - Add a client-side caching layer to reduce server-side eviction pressure — even a simple local LRU can cut eviction rates by half.
- Monitor
evicted_keysas a leading indicator — sustained eviction is the earliest signal that latency degradation is imminent. - Stagger TTLs and use background refresh to prevent cache stampededes when evicted keys need to be repopulated.
Further Reading
- Redis Eviction Keys Documentation — Official guide to all eviction policies and their behavior.
- Redis Memory Optimization Best Practices — Comprehensive coverage of
maxmemory, data structures, and tuning knobs. - Redis Lazy Freeing — Deep dive into
lazyfree-lazy-evictionand its impact on latency. - Redis Client-Side Caching — Documentation on tracking and server-assisted client caching in Redis 6+.
- Redis Cluster Specification — Understanding hash slots, key distribution, and how eviction interacts with cluster topology.
- Understanding LFU in Redis — Detailed explanation of the LFU counter and aging algorithm.