LRU cache: an interactive eviction policy simulator
An LRU cache (least recently used) holds a fixed number of items and, when it is full, evicts the one used longest ago. Every read or write moves an item to the most recent end, so the item at the other end is always next to go. Built from a hash map and a doubly linked list, both get and put take O(1) time.
The playground runs the same requests through an LRU, an LFU and a FIFO cache of four slots. Each scenario is a request pattern one of them handles better: play all three and compare the hit rates.
Hot keys and one-offs: 24 requests through three caches of 4 slots. Play it, or step through one request at a time.
How an LRU cache works
- A hash map finds any key’s entry in O(1).
- A doubly linked list keeps the entries in order of use, most recent at the head and least recent at the tail.
- On a get, look the key up in the map and move its entry to the head.
- On a put, insert or update the entry at the head. If the cache is now over capacity, remove the tail entry and delete its key from the map.
The list is what makes it O(1): moving an entry or dropping the tail is a few pointer changes, with no searching. Both structures together are a few dozen bytes per entry on top of the data.
LRU vs LFU
LRU keeps what was used recently; LFU keeps what was used often. They disagree in two classic cases. A one-time scan (a batch job or a crawler reading many keys once) pushes every popular key out of an LRU cache, because the scanned keys are more recent; LFU keeps the popular keys because their counts are higher. A shift in popularity is the opposite: LRU forgets yesterday’s popular keys as soon as they stop being used, while LFU holds them for their old counts and has too little room left for what is popular now. Production caches blend the two: segmented LRU, ARC, and W-TinyLFU all keep a small recency area and a frequency filter.
LRU vs FIFO
FIFO evicts whatever went in first, however often it has been used since. It is simpler and cheaper, since a hit changes nothing, but it evicts hot keys on schedule. In the “hot keys and one-offs” scenario LRU keeps the two hot keys and hits 58% of requests; FIFO keeps evicting them and hits 42%.
Which to use
| Policy | Evicts | A one-time scan | Popularity shifts | Seen in |
|---|---|---|---|---|
| LRU | The key used longest ago | Flushes the hot keys | Adapts at once | Memcached (a segmented LRU), most in-process caches |
| Approximated LRU | The oldest of a few sampled keys | Flushes the hot keys | Adapts at once | Redis allkeys-lru |
| LFU | The key used fewest times | Keeps the hot keys | Slow: old counts linger | Redis allkeys-lfu, with counters that decay |
| FIFO | The key that went in first | Flushes the hot keys | Adapts | Simple caches where a hit must cost nothing |
| W-TinyLFU | Whatever a frequency sketch says is least valuable | Keeps the hot keys | Adapts | Caffeine (Java) |
Redis evicts nothing by default (noeviction: writes fail when memory is full), so a Redis used as a cache needs maxmemory and a policy set; allkeys-lru is the usual choice, and allkeys-lfu suits workloads with scans.
An LRU cache in code
from collections import OrderedDict
class LRUCache:
def __init__(self, capacity: int):
self.capacity = capacity
self.items = OrderedDict() # least recently used first
def get(self, key):
if key not in self.items:
return None
self.items.move_to_end(key) # now the most recently used
return self.items[key]
def put(self, key, value) -> None:
self.items[key] = value
self.items.move_to_end(key)
if len(self.items) > self.capacity:
self.items.popitem(last=False) # evict the least recently usedOrderedDict is itself a hash map and a doubly linked list, so this is O(1). In an interview that asks you to build one (LeetCode’s “LRU Cache”), write the list yourself: a node with prev and next pointers, sentinel head and tail nodes, and the map from key to node.
In a system design interview
Size it and name the policy in one breath: “A Redis cache in front of the database, sized for the hot 20% of items that serve most reads, with allkeys-lru and a TTL for freshness; we watch the hit rate and grow it if that drops.” Expect the follow-ups: what a scan does to it (LFU, or a separate cache for batch reads), a stampede of misses when a hot key expires (request coalescing), and keeping the cache consistent with the database on writes.
Watch a strong candidate design a distributed cache, or read the caching guide for cache-aside, write-through and invalidation.
Questions people ask
What is an LRU cache?
A fixed-size cache that, when full, evicts the item used least recently. Every get or put moves an item to the most recent end, so the item at the other end is always the next to go. It keeps whatever was used lately, which suits most workloads where recent items are likely to be used again.
How do you implement an LRU cache in O(1)?
Combine a hash map with a doubly linked list. The map finds a key’s node in O(1); the list keeps nodes in order of use. A get moves the node to the head, a put inserts at the head, and when the cache is over capacity the tail node is removed and its key deleted from the map.
What is the difference between LRU and LFU?
LRU evicts the item used longest ago; LFU evicts the item used the fewest times. LRU adapts at once when popularity shifts but is flushed by a one-time scan of many keys; LFU survives scans because popular items have high counts, but holds on to items that were popular and no longer are.
When is LFU better than LRU?
When popular items stay popular and the workload includes scans or batch reads of keys used once. Those one-off keys push every popular key out of an LRU cache, while LFU keeps the popular keys because they have been used more. When popularity moves around, LRU is better.
What eviction policy does Redis use?
Whatever maxmemory-policy says, and by default that is noeviction: writes fail when memory is full. For a cache, set allkeys-lru, which evicts an approximately least recently used key by sampling a few keys, or allkeys-lfu, which uses small counters that decay over time.
What is cache thrashing?
A cache smaller than the set of keys in active use keeps evicting keys it will need again almost at once, so nearly every request misses. No eviction policy fixes it; the cache needs to be bigger than the working set, or the traffic needs to be split across caches.
What is the difference between LRU and FIFO?
FIFO evicts the item that was added first, no matter how often it has been used since, while LRU moves an item to the back of the line every time it is used. FIFO is simpler because a hit changes nothing, but it evicts hot items on schedule, so its hit rate is usually lower.