A cache with limited memory must decide which entries to evict when it's full. Compare LRU (Least Recently Used), LFU (Least Frequently Used), and TTL (Time To Live) eviction policies — how does each decide what to remove, and when would you pick one over another?
answer
- LRU=recency, linked list
- LFU=frequency, counters, decay
- TTL=staleness bound not popularity
- scan pollutes LRU
- combine TTL + LRU/LFU in practice
basics
~20 sWhen a cache runs out of space, it has to throw something away. LRU throws out the item nobody's touched in the longest time. LFU throws out the item used the fewest times overall. TTL just deletes items after a fixed amount of time, whether or not they're popular.
solid answer
~50 sLRU evicts the entry that hasn't been accessed for the longest time, tracked via a linked list or ordered map updated on every access — cheap and works well when recent access predicts future access ('recency locality'), but it's vulnerable to a single large scan flushing out a genuinely hot working set. LFU evicts the entry with the lowest access count, which better protects long-term popular items but requires tracking counts (more memory/CPU) and can get stuck favoring stale items that were popular once but aren't anymore, unless counts decay over time. TTL isn't really about popularity at all — it expires entries after a fixed duration regardless of usage, which is essential when correctness requires bounding staleness (e.g., a price that must refresh at least every 60 seconds) even if the item is still hot. In practice, systems often combine TTL with LRU or LFU: TTL bounds staleness, and the popularity-based policy governs what stays resident under memory pressure.
go deeper
Should be able to state, in plain terms, what LRU, LFU, and TTL each do to decide what gets removed.
Should explain the mechanism behind each (linked list for LRU, counters for LFU, expiry timestamp for TTL) and give one scenario where each is the right choice.
Should identify the scan-pollution weakness of LRU, the stale-popularity weakness of LFU, and explain why TTL is orthogonal to both (staleness vs. memory pressure) and often combined with them.
Should design a concrete eviction strategy for a system with mixed workloads (e.g., set TTLs for correctness, choose LFU-with-decay for memory pressure, add segmented/protected LRU for scan resistance) and justify the trade-offs with expected hit-rate impact.
## Why the eviction policy matters Every cache has finite memory, so once it fills up, adding a new entry means something existing has to be removed. The **eviction policy** is the rule that decides what to remove, and the choice matters enormously for **hit rate** — the fraction of requests the cache can satisfy without falling through to the slower origin. A poorly chosen policy can leave a cache **thrashing** (constantly evicting and refetching data that's about to be needed again), which erases most of the benefit of caching in the first place. ## Least Recently Used **Least Recently Used (LRU)** evicts whichever entry has gone the longest without being accessed. - **Mechanically, it's usually implemented with a doubly linked list plus a hash map**: every cache read or write moves that entry to the 'most recently used' end of the list, and when the cache is full, the entry at the 'least recently used' end is evicted — both operations are `O(1)`. - **LRU rests on the assumption of temporal locality**: data accessed recently is likely to be accessed again soon, which holds for a huge range of real workloads (a user re-loading their own profile, a web session revisiting the same few pages). - **LRU's well-known weakness** is that a single large sequential scan — for example, a batch job iterating over every row in a table, or a bot crawling every product page once — pushes every genuinely 'hot' entry out of the cache to make room for a flood of one-time-use entries, tanking the hit rate for real traffic right after the scan. - **Variants like LRU-K or 'segmented LRU'** (used in some database buffer pools) mitigate this by requiring an item to be accessed more than once before it's promoted to the protected, hard-to-evict segment. ## Least Frequently Used **Least Frequently Used (LFU)** evicts the entry with the lowest total access count, on the theory that popularity over time is a better predictor of future demand than mere recency. - **This directly fixes the LRU scan problem**: a one-time scan touches each item exactly once, so those items have the lowest possible frequency count and are evicted first, leaving the genuinely popular items untouched. - **The cost is bookkeeping** — LFU needs a counter per entry and (for `O(1)` eviction) a more complex data structure than a simple linked list, such as a frequency-bucketed structure. - **LFU has its own failure mode**: an item that was extremely popular last week but is irrelevant today can retain a high count and squat in the cache, starving newly-popular items — this is sometimes called 'cache pollution' by stale popularity. - **Production LFU implementations** (e.g., Redis's approximated LFU) address this with count decay — periodically halving or aging counters so old popularity fades. ## Time To Live **Time To Live (TTL)** is a different axis entirely: rather than reasoning about usage patterns, it attaches an expiration timestamp to each entry and removes it once that time has elapsed, independent of how often or recently it was accessed. TTL exists to bound staleness, not to manage memory pressure efficiently — an entry can be extremely 'hot' by LRU/LFU standards and still get evicted purely because its TTL expired. This is essential whenever correctness requires a maximum staleness window: a cached currency exchange rate, a feature flag, or a product price might be allowed to be at most 30 seconds old, no matter how popular it is. Pure TTL-based eviction, with no memory-pressure-aware policy at all, risks running out of memory if too many entries are alive simultaneously (unless combined with a max-size limit and a fallback policy). ## Combining the axes in practice In practice, these axes aren't mutually exclusive — most production caches combine them. `Redis`, for example, lets every key carry an optional TTL, and separately configures a memory-pressure eviction policy (`allkeys-lru`, `allkeys-lfu`, `volatile-ttl`, etc.) that decides what to remove once `maxmemory` is reached; a common pattern is to set business-driven TTLs for staleness correctness and let LFU (or LRU) govern which entries survive early eviction under memory pressure. Database buffer pools typically use LRU-family algorithms (MySQL's `InnoDB` uses a modified LRU with a 'young'/'old' sublist split specifically to resist the full-table-scan pollution problem) because query access patterns are strongly recency-biased at the page level. ## The choice, concretely The choice, concretely: 1. **Pick LRU** as a solid, cheap default for general-purpose caching with a recency-biased workload, and be aware of the scan-pollution risk. 2. **Pick LFU** (or a decayed variant) when your workload has a stable, skewed popularity distribution and you specifically need to protect a hot set from being flushed by transient scans or bursts. 3. **Always add a TTL** — even a generous one — whenever staleness has a real-world correctness or compliance cost, since neither LRU nor LFU on its own gives any staleness guarantee at all; an item can stay resident and 'fresh-looking' in the cache indefinitely just because it keeps getting accessed.
- Why does a full table scan hurt LRU cache performance so badly, and how do real systems like database buffer pools defend against it?A scan touches every row exactly once, so each scanned page becomes the 'most recently used' entry momentarily, pushing genuinely hot pages out of the cache to make room. Real buffer pools like InnoDB defend against this by splitting the LRU list into a 'young' (hot, protected) sublist and an 'old' sublist, requiring a page to be re-accessed after some delay before it's promoted to the young sublist — a one-off scan never gets that second touch, so it can't evict the protected hot pages.
- If a cache uses only TTL-based expiration with no size-based eviction policy at all, what production risk does that create?Without a memory-pressure-aware eviction policy, the cache can grow unbounded between expirations if write/insert volume outpaces the TTL-driven removal rate, risking an out-of-memory crash or degraded performance. This is why production caches almost always pair TTLs with a maxmemory limit and a fallback eviction policy like LRU or LFU.
- How does LFU with decay differ from plain LFU, and what problem does the decay solve?Plain LFU tracks a monotonically increasing access count per entry, which means an item that was very popular once can keep a high score forever and never get evicted even after it stops being accessed — a form of cache pollution. LFU with decay periodically ages or halves counters over time, so old popularity fades and the eviction decision reflects recent, not lifetime, demand.
LRU is like a fridge where you throw out whatever's at the back you haven't touched in ages; LFU is like keeping track of how many times each item's been eaten and tossing the least-eaten one; TTL is like a 'use by' date stamped on the container regardless of how much you love that leftovers dish.
saying these in an interview costs you the question
- Says LRU and LFU are basically the same thing
- Thinks TTL is a memory-management policy rather than a staleness bound
- Doesn't know a full scan can flush an LRU cache's hot set
- Assumes eviction policies are mutually exclusive and can't be combined
- Can't explain the O(1) LRU implementation via linked list + hash map