Eviction Policies
Caches have limited memory. When full, something must go. The eviction policy decides what gets removed.
Choose wrong, and your cache hit ratio tanks. Choose right, and you maximize performance with limited resources.
Why Eviction Matters
The goal: Keep the data most likely to be requested again.
LRU: Least Recently Used
The most popular eviction policy. Evicts the item that hasn't been accessed for the longest time.
Intuition
If you haven't looked at something recently, you probably won't need it soon. Like cleaning your desk - papers you haven't touched in months go first.
from collections import OrderedDict
class LRUCache:
def __init__(self, capacity):
self.capacity = capacity
self.cache = OrderedDict()
def get(self, key):
if key not in self.cache:
return None
# Move to end (most recently used)
self.cache.move_to_end(key)
return self.cache[key]
def put(self, key, value):
if key in self.cache:
self.cache.move_to_end(key)
self.cache[key] = value
if len(self.cache) > self.capacity:
# Remove oldest (least recently used)
self.cache.popitem(last=False)
LRU Visualization
Cache capacity: 3 items
Access: A → Cache: [A]
Access: B → Cache: [A, B]
Access: C → Cache: [A, B, C] (full)
Access: A → Cache: [B, C, A] (A moved to end)
Access: D → Cache: [C, A, D] (B evicted - least recent)
Pros
- Simple to understand and implement
- Works well for most access patterns
- O(1) operations with proper data structures
Cons
- Doesn't consider frequency - a single access resets position
- Scan pollution: sequential scans evict hot data
LFU: Least Frequently Used
Evicts the item with the lowest access count.
Intuition
Items accessed many times are probably important. Keep the popular ones, evict the rarely-used ones.
from collections import defaultdict
class LFUCache:
def __init__(self, capacity):
self.capacity = capacity
self.cache = {} # key -> value
self.freq = {} # key -> frequency
self.freq_to_keys = defaultdict(list) # freq -> [keys]
self.min_freq = 0
def get(self, key):
if key not in self.cache:
return None
# Increase frequency
old_freq = self.freq[key]
self.freq[key] += 1
self.freq_to_keys[old_freq].remove(key)
self.freq_to_keys[old_freq + 1].append(key)
# Update min_freq if needed
if not self.freq_to_keys[self.min_freq]:
self.min_freq += 1
return self.cache[key]
Pros
- Protects frequently accessed items
- Better for skewed access patterns
- Resistant to scan pollution
Cons
- More complex to implement
- "Frequency pollution" - old popular items stick around
- New items get evicted quickly
An item accessed 1000 times last month but never since will stay in cache forever while new popular items get evicted. Solution: Use time-decayed frequency (TinyLFU).
TTL: Time To Live
Items expire after a fixed duration, regardless of access patterns.
import time
class TTLCache:
def __init__(self):
self.cache = {} # key -> (value, expiry_time)
def set(self, key, value, ttl_seconds):
expiry = time.time() + ttl_seconds
self.cache[key] = (value, expiry)
def get(self, key):
if key not in self.cache:
return None
value, expiry = self.cache[key]
if time.time() > expiry:
del self.cache[key]
return None # Expired
return value
Pros
- Guarantees data freshness
- Simple and predictable
- Perfect for time-sensitive data
Cons
- Popular items may expire when still needed
- Mass expiration can cause thundering herd
- Choosing the right TTL is tricky
Choosing TTL Values
| Data Type | Typical TTL | Reasoning |
|---|---|---|
| User session | 30 min - 24 hours | Security vs convenience |
| Product catalog | 1 - 24 hours | Prices/inventory change |
| API rate limit | 1 minute - 1 hour | Depends on limit window |
| Static assets | 1 year | Version in URL for updates |
| News articles | 5 - 15 minutes | Breaking news updates |
FIFO: First In, First Out
Simple queue-based eviction. Oldest items go first.
Add A → [A]
Add B → [A, B]
Add C → [A, B, C] (full)
Add D → [B, C, D] (A evicted - oldest)
Simple scenarios where recency and frequency don't matter. Rarely optimal, but easy to implement.
Random Eviction
Surprisingly effective! Just pick a random item to evict.
Pros
- O(1) - no tracking overhead
- No pathological worst cases
- Works surprisingly well in practice
Cons
- May evict hot items by chance
- Not optimal for skewed workloads
Policy Comparison
| Policy | Best For | Complexity |
|---|---|---|
| LRU | General purpose, most workloads | Medium |
| LFU | Skewed access, protecting hot keys | High |
| TTL | Time-sensitive data, freshness guarantees | Low |
| Random | Simple systems, uniform access | Very Low |
Real-World: Redis Eviction Policies
Redis offers multiple policies you can configure:
# In redis.conf
maxmemory 100mb
maxmemory-policy allkeys-lru
Redis Eviction Policies
noeviction- Return errors when fullallkeys-lru- LRU across all keysvolatile-lru- LRU only among keys with TTLallkeys-lfu- LFU across all keysvolatile-ttl- Evict keys with shortest TTL
Key Takeaways
- LRU is the default choice - simple and effective for most cases
- LFU protects hot keys but watch for frequency pollution
- TTL guarantees freshness - combine with LRU/LFU for best results
- Profile your access patterns before choosing - one size doesn't fit all
Next up: Cache Invalidation - The hardest problem in computer science.