System Design (High-Level Design) Module 7 – Caching
Eviction policies and cache sizing
Why a full cache must forget, how LRU, LFU, FIFO, random, TTL expiry and admission filters choose, and how to size a cache from its hit-ratio curve.
What you will learn
- Compare LRU, LFU, FIFO, random eviction, TTL expiry and W-TinyLFU admission
- Estimate a cache's hit ratio from its size and the skew of its reads
- Size a cache from the database's limit and the cost of an entry
- Implement an LRU cache whose get and put take constant time
Before you start
What you’ll learn · Before you start
What you will learn
- Compare LRU, LFU, FIFO, random eviction, TTL expiry and W-TinyLFU admission
- Estimate a cache's hit ratio from its size and the skew of its reads
- Size a cache from the database's limit and the cost of an entry
- Implement an LRU cache whose get and put take constant time
Before you start
On this page
A 2 GB cache in front of 20 GB of product records can hold a tenth of them, so once it is full, every new entry pushes an old one out. Two decisions follow, and they belong together: which entry leaves, and how much memory the cache gets in the first place. eviction policy: The rule a full cache follows to choose which entry leaves when a new one needs room, such as LRU, LFU, FIFO or random eviction. make the first decision; cache sizing makes the second.
Why a cache has to forget
Memory is the cache’s whole budget. A catalogue of 20 million products cannot sit in a few gigabytes, so the cache keeps only part of it, and the part it keeps decides its hit ratio: The share of lookups a cache answers itself: hits divided by hits plus misses over a period. At 95 % of 20,000 lookups a second, 1,000 a second still reach the layer behind it. More in “Where caches live, from browser to database”. That ratio grows with the cache’s size, but not evenly: the first entries hold the most popular items and add many hits each, while the last ones hold items that are rarely read.
Hit ratio grows with size, then flattens
Text description of the diagram
A chart with cache size on the horizontal axis and hit ratio on the vertical axis, without numbers.
- The curve starts at zero and rises steeply: the first entries the cache can hold are the most popular items, so each one adds a lot of hits.
- It then bends and flattens: the entries added last hold items that are rarely read, so each one adds only a few hits.
- A dashed line near the top marks the most a cache can reach even when it holds every item, because the first read of each item is always a miss. The curve approaches the line but never reaches it.
This trace shows the flattening with real numbers. It replays 400,000 reads of 20,000 items, the popular ones read far more often, through LRU (least recently used): A cache policy that, when the cache is full, evicts the entry that has gone longest without being read. It keeps what was read recently, which is close to, but not the same as, keeping the most popular items. More in “Estimating storage, bandwidth and cache memory” caches of seven sizes:
"""Hit ratio of an LRU cache by size, on one seeded trace: 400,000 reads of 20,000 items whose popularity follows a
Zipf-like curve (the item of rank k is read in proportion to 1 / k**0.9). The same seed gives the same trace, so the
table is the same on every run."""
import random
from collections import OrderedDict
from itertools import accumulate
ITEMS, READS = 20_000, 400_000
rng = random.Random(11)
cumulative = list(accumulate(1 / k**0.9 for k in range(1, ITEMS + 1)))
trace = rng.choices(range(ITEMS), cum_weights=cumulative, k=READS)
def lru_hit_ratio(capacity):
cache, hits = OrderedDict(), 0
for key in trace:
if key in cache:
hits += 1
cache.move_to_end(key) # the most recently used goes to the end
else:
cache[key] = True
if len(cache) > capacity:
cache.popitem(last=False) # forget the least recently used
return hits / READS
print(f"{'entries':>8} {'of items':>9} {'hit ratio':>10} {'points per 1,000 more entries':>31}")
previous = None
for size in [200, 500, 1_000, 2_000, 4_000, 8_000, 16_000]:
ratio = lru_hit_ratio(size)
gain = "" if previous is None else f"{(ratio - previous[1]) * 100 / ((size - previous[0]) / 1_000):.1f}"
print(f"{size:>8,} {size / ITEMS:>9.1%} {ratio:>10.1%} {gain:>31}")
previous = (size, ratio) Output
entries of items hit ratio points per 1,000 more entries
200 1.0% 28.9%
500 2.5% 38.9% 33.5
1,000 5.0% 47.7% 17.4
2,000 10.0% 57.3% 9.7
4,000 20.0% 68.2% 5.4
8,000 40.0% 80.3% 3.0
16,000 80.0% 92.6% 1.5
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 hit_curve.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Holding 5 % of the items answers almost half the reads. Each further 1,000 entries then adds less: 33.5 points at first, 1.5 points near the end. So cache sizing is never “as big as possible”; it is the point where the next gigabyte stops paying for itself. The curve is not a law of nature either. It comes from the skew of your own reads, which is why the same memory can answer half the reads in one system and nearly all of them in another.
Eviction policies decide which entry leaves when a new one needs room. The common ones are short to state:
- LRU evicts the entry that has gone unused for the longest time.
- LFU evicts the entry that has been read least often.
- FIFO evicts the oldest entry, however often it is read.
- Random eviction drops an entry chosen at random, which costs no bookkeeping at all.
- TTL expiry removes an entry when its lifetime ends, whether or not memory is short.
- An admission filter such as W-TinyLFU decides whether a new item may enter at all, from an estimate of how often it is read.
On the same memory, eviction policies can differ by ten points of hit ratio or more, and the best one depends on how the reads behave over time. Cache sizing and the policy are therefore one decision: a better policy needs less memory for the same hit ratio.
Checkpoint
Check: Why a cache must forget
How LRU, LFU, FIFO and random choose a victim Pro: opens with a Pro, Premium or Ultimate pass
In this part:
- Admission filters: TinyLFU and W-TinyLFU
- Five policies measured on three traces
- Sizing a cache from a hit-ratio curve, with numbers
- Redis eviction policies and the noeviction trap
- Practise: build an LRU cache with constant-time get and put
- Build an LRU cache whose get and put take constant time
- Interview questions
- Size a cache for a catalogue of 50 million items read 20,000 times a second, where a miss costs a 10 ms database read. How much memory do you ask for, and how do you justify it?
- A nightly report reads every product once, and the next morning the product cache's hit ratio has fallen from 92 % to 40 %. Why, and which policy or design avoids it?
- A Redis cache configured with noeviction fills up at peak traffic. What do users see, and how should the configuration and the code change?
- Key takeaways
3 diagrams · 2 runnable examples (Python) · 1 exercise with sample tests · 3 interview answers · about 49 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Terms in this lesson
- admission filter
- A cache policy that decides whether a new item may enter the cache at all, from an estimate of how often it is read. W-TinyLFU is one.
- eviction policy
- The rule a full cache follows to choose which entry leaves when a new one needs room, such as LRU, LFU, FIFO or random eviction.
- FIFO eviction
- A cache policy that evicts the oldest entry, the first one stored, however often it is read.
- hit-ratio curve
- A cache's hit ratio drawn against its size. It rises steeply for the first entries, which hold the most popular items, and flattens for the last ones, which hold items rarely read.
- LFU (least frequently used)
- A cache policy that, when the cache is full, evicts the entry that has been read least often.
References
- TinyLFU: A Highly Efficient Cache Admission Policy (arXiv (Einziger, Friedman and Manes))
- Efficiency (Caffeine wiki) (Caffeine (Ben Manes))
- Key eviction (Redis documentation) (Redis)
- Web Caching and Zipf-like Distributions: Evidence and Implications (IEEE INFOCOM (Breslau, Cao, Fan, Phillips and Shenker; authors' page))
- collections.OrderedDict (Python Software Foundation)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress