Systems Engineering

WritingSystem Design

Cache Eviction: What to Forget When It's Full

The three eviction policies up close: LRU (drop the least recently used), LFU (drop the least frequently used), and TTL (expire on a timer). Why the same full cache evicts a different item under each, and what real systems like Redis actually run.

By Sachin Gupta9 min read
Portrait of Sachin Gupta rendered in binary

A dedicated look at cache eviction. Why a cache must forget, then the three policies each with its own everyday analogy and interactive visualizer: LRU (recency, a desk), LFU (frequency, a bestseller shelf), and TTL (age, milk with an expiry date). A worked example for each, a hand-drawn diagram showing the same cache evict three different items, guidance on which to use when, and what Redis and memcached run in production.

In the caching piece we met a cache: a fast copy of an answer kept close by. We also met its catch. A cache is small, so it is almost always full, and to make room it has to forget something. We used LRU there as the default. Now we put all three eviction policies side by side, each in full, because each is really its own idea.

Think of a small bag you carry every day. It holds four things, and it is always full. The moment you want to add a fifth, you have to take one out. Which one do you drop? You could drop the thing you have not touched in the longest time. Or the thing you reach for the least overall. Or the thing that has simply gone off, like a snack past its date. Those are three genuinely different rules, and each is a real eviction policy. There is no single right answer, only a right answer for a given pattern of use.

That is the whole of eviction: a full cache, and one question, "which one goes?" Everything below is three ways to answer it.

LRU: drop the least recently used

Picture a small desk that holds four things. Every time you touch one, you slide it to the front. When you reach for a fifth and the desk is full, the thing at the very back, the one you have ignored the longest, goes back on the shelf. That is LRU, least recently used. Its bet is simple: what you touched recently, you will probably touch again soon, so keep the recent stuff and drop the stale stuff.

Worked example. The desk holds [A, B, C, D], most recent at the front. You use A, so it slides to the front: [A, B, C, D] becomes [A, ...] with the rest shifting back. Now a new item E arrives and the desk is full. The item at the back, D, has gone untouched the longest, so D is evicted and E takes the front. Recency won: D left not because it was unpopular overall, but because it had been quiet lately.

Interactive · LRU evictionOpen in Visualizers →
LRU: a full cache forgets the coldestauto
accessing-
most recentleast recent (evicted first)

A 4-slot cache. Watch it fill, then evict the least recently used to make room.

LRU is the workhorse, the sensible default, and the one most systems reach for first. It matches how a lot of real traffic behaves: a burst of interest in something, then silence.

LFU: drop the least frequently used

Now change the rule. Picture a small bestseller shelf in a bookshop. What earns a spot is not when a book last sold, but how many copies it has sold in total. A steady favorite that sells every week keeps its place for months. A book that had one lucky day and then nothing gets pulled, even though that one day was recent. That is LFU, least frequently used: every item keeps a running count of how often it has been asked for, and when the cache fills, the item with the lowest count leaves.

The difference from LRU is the whole point. LRU asks "when did you last use this?" LFU asks "how often do you use this, ever?" So an item touched long ago but very often survives, while a fresh newcomer with a count of one is the first to go.

Worked example. The cache holds four items with these use-counts: A used 2 times, B used 50 times, C used 3 times, D used 8 times. A new item E arrives and the cache is full. LFU drops the lowest count, which is A at 2, even though A might have been used a moment ago. B, used 50 times, stays easily, even if its last use was hours back. Frequency won.

Interactive · LFU evictionOpen in Visualizers →
LFU: the least-used item leavesauto
accessing-
ranked by use count, not by recencylowest count leaves first

A 4-slot cache that ranks items by how many times each has been used. Watch it fill, then drop the least used.

LFU shines when popularity is lopsided and durable: a small set of items is genuinely hot for a long time (think a handful of viral videos, or the top search queries). It has one well-known weakness, called cache pollution or aging: an item that was popular last month can build up a huge count and then linger long after interest has died, because its old score keeps it safe. Real systems fix this by letting counts decay over time, which we will see in a moment.

TTL: expire on a timer

Now a completely different rule, one that does not care about use at all. Picture the milk in your fridge. It has a date stamped on it. When that date passes, out it goes, whether you drank a glass this morning or have not touched it in a week. That is TTL, time to live: each item is stored with a countdown, and when the timer reaches zero the item expires and is evicted, regardless of how recently or how often it was used.

TTL answers a different worry from LRU and LFU. Those two are about space, "we are full, make room." TTL is about freshness, "this answer is only trustworthy for so long." A stock price, a weather reading, a login session, a rendered feed: each is fine to reuse for a few seconds or minutes, then it should be thrown out and fetched again so nobody sees a stale value.

Worked example. You cache a currency rate with a TTL of 30 seconds. For 30 seconds every request gets the cached rate, fast and cheap. At the 30-second mark it expires and is evicted, even if a thousand people are reading it right then. The next request misses, fetches a fresh rate, and caches it again with a new 30-second timer. The item left on age, not on pressure for space.

Interactive · TTL evictionOpen in Visualizers →
TTL: items expire on a timerauto
tick: every timer counts down
A4s left
B3s left
C2s left
D5s left
expiry is about age, not use: the timer decides, not the last read

Every item carries a timer. Each tick all timers count down; when one hits zero it expires and leaves, whatever else is true about it.

Because TTL is about age rather than space, it is not really a competitor to LRU and LFU; it sits alongside them. A real cache often runs both: a TTL so nothing is ever too stale, and LRU or LFU to decide what to drop early when memory runs out before the timers do.

The same cache, three different victims

Here is the cleanest way to feel the difference. Take one cache, in one exact state, and ask all three policies which item to evict. They disagree, because they are measuring three different things.

The same four-item cache viewed three ways. Each item shows three facts: when it was last used, how many times it has been used, and how long until it expires. Item A was used just now, only 2 times, and expires in 30 seconds. B was used 5 minutes ago, 50 times, expires in 2 minutes. C was used 1 minute ago, 3 times, expires in 5 seconds. D was used 10 minutes ago, 8 times, expires in 45 seconds. LRU drops D, the one used longest ago. LFU drops A, the one used the fewest times. TTL drops C, the one whose timer runs out soonest. One cache, three policies, three different victims.

Same four items, same instant, three answers. LRU evicts D (quiet the longest). LFU evicts A (used the fewest times overall). TTL evicts C (its timer runs out first). None of them is wrong. They are tuned to different questions, and picking a policy means picking which question matches your traffic.

Which one to use when

A short guide, and then what real systems actually do.

A table of the three eviction policies. LRU evicts the item untouched the longest; it is best for general-purpose, recency-heavy traffic and is the safe default, but watch out that a one-time scan of many items can flush the hot set. LFU evicts the item used the fewest times; it is best when popularity is lopsided and lasts, like viral content or top queries, but watch out for aging, where stale-but-once-popular items linger unless counts decay. TTL evicts any item whose timer hits zero; it is best for data that goes stale by the clock, like prices, sessions, or feeds, but watch out that too long serves stale data while too short kills the hit rate.

The honest summary: reach for LRU unless you have a reason not to. Add LFU when a small set of items stays hot for a long time and you want to protect it. Add TTL almost always, on top of either, because most cached data has a shelf life and you would rather it expire on a timer than be served stale.

What production actually runs

Two things are worth knowing, because they come up the moment you configure a real cache.

First, real caches do not track perfect order. Keeping an exact, globally sorted "least recently used" list for millions of keys would cost more memory and time than the cache saves. So systems approximate. Redis, for example, does not scan every key to find the true LRU or LFU victim; it samples a small handful of random keys and evicts the worst one among them. You control how many with maxmemory-samples (default 5); raising it to 10 gets closer to true LRU at a little more CPU. It is a deliberate trade of a touch of accuracy for a lot of speed.

Second, the policy is a setting, not a rewrite. In Redis you pick one with maxmemory-policy, and the names map straight onto this article: allkeys-lru and allkeys-lfu apply the rule to every key, volatile-lru and volatile-lfu only to keys that have a TTL set, and volatile-ttl evicts the key whose TTL is nearest first. Redis LFU is the approximate kind: it stores frequency in a small probabilistic counter (a Morris counter) that increments slowly and, importantly, decays over time, which is the built-in fix for the aging problem we saw earlier. Memcached, the other classic, runs LRU. The concepts are exactly the three above; the products just wrap them in config flags.

Next: eviction handles a cache that is too full. The harder problem is a cache that is wrong, serving an answer that is no longer true. That is cache invalidation, and its nasty cousin, the stampede that hits when a hot key expires and every request rushes the database at once. That is the next piece.

Sources: Redis, Key eviction.

Related