Engineering Atlas › System Design Fundamentals

Deciding what to evict: LRU, LFU and friends

15 min read · last reviewed 2026-10-17

Open in the interactive app: practice, quiz, mock interview →

In one minute

How it works

What the policy is trying to do

Every eviction bets that this entry will be needed later than the others. Belady showed in 1966 that evicting the entry used farthest in the future minimises misses. No online cache knows the future, but simulating MIN on a recorded trace bounds the possible gain: if LRU misses 12% and MIN 9% at the same size, no policy saves more than a quarter of your misses.

Recency: LRU and its weakness

LRU evicts the entry unused for the longest time and adapts instantly when popularity shifts. Its weakness is scans: a one-off pass over many keys (a backup, a crawler, an analytics query) pushes every hot key out for keys that will never be read again. A loop slightly larger than the cache makes it miss on every access. The LLD lesson on designing an LRU cache covers its implementation.

Frequency: LFU and its weakness

LFU evicts the entry with the fewest accesses, so scans barely affect it. But a pure count never forgets: yesterday's viral video keeps its count long after interest moves on, so practical LFU ages counts. Redis's allkeys-lfu keeps an 8-bit logarithmic counter per key that is incremented probabilistically (lfu-log-factor, default 10, saturates around a million hits) and decremented when the key has been idle for lfu-decay-time minutes (default 1) (Redis docs). LFU can be O(1) with a list of frequency buckets, moving a key to the next bucket on each hit (Shah, Mitra and Matani).

Approximations: CLOCK and sampling

CLOCK approximates LRU with a circular buffer and a reference bit per slot. A hit sets the bit; to evict, a hand sweeps the circle, clearing set bits and evicting the first slot whose bit is clear. PostgreSQL's buffer manager uses a generalised version, the clock sweep, with a usage_count per buffer that a hit increments up to 5 and the sweep decrements, so frequently used pages survive several passes. Postgres also fences off scans instead of trusting the policy: a large sequential scan reuses a private 256 KB ring of buffers, and COPY uses a ring of at most 16 MB, so one big query cannot flush shared_buffers (Postgres buffer README). Redis avoids lists entirely: it samples maxmemory-samples keys (default 5) and keeps a small pool of the best candidates across evictions. Redis 8.6 added a third signal, least recently modified (allkeys-lrm, volatile-lrm): the timestamp moves only on writes, so keys that are read constantly but never updated still age out (Redis docs).

Combining both signals

2Q and segmented LRU put new entries on a probationary list and promote them on a second hit, so scans churn only probation. ARC (Megiddo and Modha, 2003) keeps recency and frequency lists plus "ghost" lists of evicted keys, and shifts space towards whichever ghost list gets hits (ARC paper). Linux uses active and inactive lists, and since kernel 6.1 offers the multi-generational LRU (MGLRU).

Suspicion of newcomers: TinyLFU, S3-FIFO and SIEVE

Objects requested once and never again (one-hit wonders) are common, and more so at cache scale. Across 6,594 production traces, the median share of one-hit wonders was 26% over a whole trace but 72% within a slice holding 10% of the trace's objects, which is roughly what a cache sees between evictions (S3-FIFO paper). Recent policies get most of their gain by evicting these fast.

W-TinyLFU, used by Caffeine and adopted by many Java and Go caches, adds an admission step. A small LRU window catches bursts. The paper's Caffeine release fixed it at 1% of capacity (TinyLFU paper); current Caffeine sizes window and main regions at runtime by hill climbing on the hit rate (Caffeine design). When the window evicts a candidate, a compact frequency sketch decides whether it beats the main region's victim; if not, the newcomer is dropped and the established entry stays. The sketch is a Count-Min sketch of 4-bit counters, halved periodically so old popularity fades.

S3-FIFO (SOSP 2023) uses three FIFO queues: a small one (about 10% of the cache) for new objects, a main one, and a ghost queue of recently evicted keys. Objects not re-accessed while in the small queue are evicted quickly; objects found in the ghost queue go straight to main.

SIEVE (NSDI 2024) is simpler still: one FIFO queue with a visited bit per object. A hit sets the bit without moving the object. To evict, a hand walks from tail toward head, clearing bits and evicting the first unvisited object, and stays where it stopped. An object never re-read is evicted on the hand's next pass. On 1,559 traces it had a lower miss ratio than nine other modern policies on over 45% of traces, and adding it to five production cache libraries took fewer than 20 changed lines on average (SIEVE paper).

Expiry comes before eviction

In caches with TTLs, many entries are dead before the policy ever looks at them. Twitter's study of 153 production cache clusters found that TTLs often set the effective working set, so removing expired objects promptly matters more than the choice of eviction policy, and at reasonable sizes FIFO often matched LRU (Twitter OSDI 2020). Redis, for example, deletes expired keys lazily on access and also periodically samples keys with TTLs to delete the dead ones nobody reads (Redis docs).

The same problem shape elsewhere

Eviction is the overload shape applied to memory: when demand exceeds capacity, drop by predicted value. An LLM server dropping KV-cache blocks of idle conversations and a Kubernetes node evicting pods under memory pressure make the same bet, and fail the same way when a burst of low-value work pushes out high-value work.

Down the stack

Layer 1The contract

An eviction policy promises nothing about any particular key. It promises bounded memory (entries or bytes never exceed the limit), near-constant time per operation, and implicitly a miss ratio close to what the workload allows. What can be tested is behaviour on a trace: for a given capacity, hits and evictions are deterministic for LRU, LFU, CLOCK and SIEVE, and probabilistic for sampled policies such as Redis's.

Layer 2The mechanism

LRU is a hash map plus a doubly linked list: two pointers per entry and six pointer writes per hit. O(1) LFU is a list of frequency nodes, each owning a list of entries, plus a hash map from key to entry; a hit unlinks the entry and appends it to the next frequency node, creating it if needed. CLOCK is an array of slots, a bit per slot and one hand index. SIEVE is a FIFO list, a visited bit per node and a hand pointer that persists between evictions. W-TinyLFU's sketch is four rows of 4-bit counters, eight bytes of sketch per cache entry in Caffeine (Caffeine design), indexed by hashes of the key; an estimate is the minimum of the four counters, and when the number of recorded accesses reaches a sample size (ten times the maximum entries in the original paper), every counter is halved. Redis stores a 24-bit field in each object header: a minutes-resolution clock for LRU, or a 16-bit decay timestamp plus the 8-bit counter for LFU.

Layer 3The machine

The hidden cost of LRU is that a read becomes a write. Moving a node to the head writes to the node, its two neighbours and the head, dirtying several 64-byte cache lines that other cores then have to fetch back, and the list needs a lock. On a 32-core server the lock and cache-line traffic, not the hash lookup, cap throughput. Caffeine works around this by recording hits in striped, lossy ring buffers and replaying them under a lock in batches. CLOCK and SIEVE avoid the problem: a hit is a single store to a bit or byte that is often already set, so reads scale with cores. The SIEVE authors report twice the throughput of an optimised LRU at 16 threads (SIEVE paper).

Metadata matters at scale: two 8-byte list pointers for 100 million small objects is 1.6 GB. A FIFO can be a ring of indices and a sketch costs a few bytes per entry, which is why Redis squeezes recency into 24 bits of the object header.

Why it's built this way

  1. Memory is finite and misses are expensive, so every cache needs a rule for what to drop.
  2. The optimal rule needs the future, so policies predict it from recency and frequency.
  3. Recency alone is fooled by scans and frequency alone by stale popularity, so good policies combine them or filter newcomers.
  4. Most new objects are never reused, so evicting or rejecting them early gives most of the gain cheaply.
  5. Updating shared order on every hit limits multicore throughput, so modern policies keep hits to a bit flip and do the work at eviction time.

Diagram

flowchart TD
  M["Miss and cache full"] --> H["Hand at object (start: tail)"]
  H --> V{"Visited bit set?"}
  V -->|yes| C["Clear bit, move hand toward head (wrap at head)"]
  C --> V
  V -->|no| E["Evict object, hand stays at its neighbour"]
  E --> I["Insert new object at head, bit clear"]
  HIT["Hit"] --> S["Set visited bit, do not move"]

Worked example

A product image cache at an edge site holds 2 million objects. Normal traffic is 20,000 requests per second, and a nightly partner sync walks 5 million distinct catalogue images once, at 5,000 requests per second for about 17 minutes.

Replaying a day of logs through a simulator gives these miss ratios (illustrative numbers in the shape such studies usually show). Belady's MIN: 8%. W-TinyLFU: 10%. SIEVE: 10.5%. LRU: 13% for the day, but during the sync window LRU's miss ratio for normal traffic jumps to 40%, because the scan pushes most of the hot set out, and it takes about an hour of normal traffic to refill.

Translate to the origin. LRU at 13% sends 2,600 requests per second to the image origin on average, and the post-sync refill spikes it toward 8,000. W-TinyLFU at 10% sends 2,000, and during the sync it rejects the one-off images at admission, so the hot set barely moves. SIEVE also protects the hot set: hot objects have their visited bit set, so the hand clears it and moves on, while scanned objects are evicted on the hand's next pass.

Both cut origin load by about a quarter versus LRU and remove the nightly spike; SIEVE needs one bit per object, W-TinyLFU gets slightly better ratios. Either beats buying 30% more memory, which the miss-ratio curve says would only bring LRU to about 11.5%.

In the wild

Trade-offs

PolicyStrengthsWeaknessesChoose when
LRUSimple, adapts quicklyScan and loop pollution, hits need lockingSmall caches, recency-driven workloads
LFU with agingScan resistant, keeps steady favouritesSlow to adapt, tuning of decayStable popularity, such as reference data
CLOCKCheap hits, approximates LRUSame scan weakness as LRUBuffer pools, page caches
ARC or 2QBalances recency and frequencyMore metadata, ghost listsStorage caches with mixed scans
W-TinyLFUNear-optimal on many traces, scan resistantSketch and window add complexityLibrary caches (Caffeine, Ristretto)
SIEVE or S3-FIFOVery simple, cheap hits, evicts one-hit wondersLess suited to loops with heavy reuseWeb and key-value caches with high churn

Pitfalls in production

Leadership lens

Check yourself

  1. Why can LRU miss on every access when a workload loops over 1,001 keys with a cache of 1,000?

    Show answer

    Each key is the least recently used exactly when it is about to be needed, so it is always evicted just in time to miss. MIN would keep most keys resident.

  2. Why does a hit in SIEVE scale better across cores than a hit in LRU?

    Show answer

    A SIEVE hit only sets a visited bit, while an LRU hit relinks list nodes, which needs a lock and writes several shared cache lines.

  3. What problem does TinyLFU's admission step solve that eviction alone does not?

    Show answer

    It stops a newcomer from displacing an entry that is accessed more often. A scan can then pass through the small window without evicting the established hot set.

  4. Why do LFU caches need aging?

    Show answer

    Without it, items that were popular long ago keep high counts and never leave, so new popular items cannot get in.

Sources and further study

Primary

Systems docs

Books

Other curricula

Watch

Free resources

Open in the interactive app: practice, quiz, mock interview →