One question that recently made me rethink cache eviction was this one.
If Redis uses LRU, why doesn't it maintain a heap, or a perfectly sorted list, of keys?
The answer comes down to optimizing the common case.
What a perfect ordering would cost
Imagine a Redis instance with 10 million keys. If Redis maintained a perfect LRU structure, every cache hit would have to update it.
Even though the lookup itself is O(1), updating a heap would be O(log n), and maintaining a doubly-linked LRU list would still mean modifying shared metadata on every single read. For a cache serving millions of requests per second, that is expensive — and it is expensive on the path that runs most often.
What Redis does instead: sample five
Rather than maintaining an exact ordering, Redis uses random sampling. When memory is full and a write arrives:
- Randomly sample a small number of keys — five, by default.
- Find the least recently used among them.
- Evict that key.
At first glance this seems inaccurate. What if all five sampled keys are hot? It's possible — but statistically very unlikely for most real-world workloads, and the cost of being wrong is one slightly premature eviction rather than a wrong answer.
The math: how unlikely are five hot keys?
A hot key is one that will be read again soon. If a share h of keys is hot, one random key is hot with chance h, and five are all hot with chance h × h × h × h × h = h5. The hash function scattered the keys, so five neighbours in the table are as good as five independent picks.
share hot all five hot about one
(h) (h^5) eviction in
10% 0.00001 100,000
20% 0.00032 3,125
50% 0.03125 32
80% 0.32768 3
Real traffic is skewed, so h is small and the top rows apply. The pool only lowers these odds, since it also holds colder keys from earlier rounds. The bottom row is where sampling struggles: raising maxmemory-samples to 10, as Redis suggests, takes it from 1 in 3 to about 1 in 9.
And “random” is looser than it sounds. To see why, picture how Redis stores keys. A Redis database is one big dictionary from key names to values, and underneath it sits a hash table: a long array of numbered positions in memory, called buckets. To store user:42, Redis runs the name through a hash function, which turns it into a bucket number, and puts the key in that bucket; to read it back, it hashes the name again and goes straight there. Redis adds buckets as keys arrive, so a bucket holds about one key, and many buckets are empty.
Sampling walks that array. Redis does not pick five buckets at random: it picks one random bucket and steps right, bucket by bucket, until it has collected five keys. Only the starting bucket is random, and that is still fair: the hash function placed each key by its name alone, not by how often it is read, so keys in neighbouring buckets have nothing to do with each other. Stepping to the next bucket is also much cheaper than jumping to a new random one.
If you run Redis Cluster, don’t confuse those buckets with its 16,384 hash slots. A slot is how the cluster splits keys between servers: every key belongs to one slot (CRC16 of the key, mod 16,384), and each server owns a share of the slots. A bucket lives inside one server’s hash table: since Redis 7.4 every slot gets its own table, which adds buckets as its keys grow, so a bucket still holds about one key. Eviction picks a random slot first, then walks that slot’s buckets.
The eviction pool
Redis goes one step further. Instead of discarding the remaining sampled keys after each eviction, it keeps the best candidates in a small eviction pool — sixteen entries, internally.
Need more memory? The next round samples five fresh keys and merges them into what the pool already holds.
The pool accumulates better candidates while the sampling cost stays flat. Every round still reads only a handful of random keys, but the set it chooses from has been filtered by every round before it.
That does not mean the pool grows forever. It has sixteen slots, kept in order from the key used most recently to the key unused longest. Once all sixteen are full, a sampled key gets in only if it has gone unused longer than the pool’s most recently used key, which it pushes out.
Why this works
Redis is optimizing for the shape of real traffic: millions of GETs, and relatively few evictions. Paying a maintenance cost on every read buys accuracy in the rare event and charges for it in the common one. Sampling inverts that — it does a small amount of work only when memory is exhausted.
Accept a near-perfect approximation during rare events to keep the hot path extremely fast.
That is one of the reasons Redis continues to scale so well while delivering hit rates remarkably close to a true LRU implementation.