system-design · intermediate

Cache Eviction Policies — LRU, LFU, TTL, and Friends

The Central Question

Consider an in-memory Redis cluster deployed for an e-commerce platform on the CacheLab platform (cachelab.com) provisioned with 64 GB of RAM:


The cache cannot expand beyond its 64 GB physical memory limit. It faces an immediate operational decision: Which existing cached item should be evicted from RAM to free space for Product #9941?

If the cache evicts a highly popular product record (such as an iPhone on sale receiving 5,000 QPS), subsequent user requests will miss the cache, slamming the underlying PostgreSQL database with thousands of concurrent queries and triggering a total site outage.

If the cache evicts a cold, rarely accessed record (such as an obscure product description requested once three days ago), database load remains minimal and API latency stays under 2 milliseconds.

The algorithm governing this selection is a Cache Eviction Policy (Replacement Algorithm).

This lesson answers one central question: How do cache eviction algorithms (LRU, LFU, FIFO, W-TinyLFU) manage finite RAM boundaries to maximize hit ratio, eliminate scan pollution, and execute in $O(1)$ time complexity using Hash Maps and Doubly-Linked Lists?


Expiry (TTL) vs. Eviction (Capacity)

Engineers frequently confuse Expiry with Eviction. They are two separate, independent mechanisms operating inside a cache:

flowchart TD
  CacheSpace[Cache Space Management] --> Expiry[1. Time-Based Expiry TTL]
  CacheSpace --> Eviction[2. Space-Based Capacity Eviction]
  
  Expiry --> ExpiryDesc["Triggered by TIME.<br/>Removes items when Time-To-Live expires.<br/>Controls DATA FRESHNESS."]
  Eviction --> EvictionDesc["Triggered by MEMORY CAPACITY.<br/>Removes items when RAM limit is reached.<br/>Controls MEMORY CONSUMPTION."]

Figure 1: Conceptual distinction between Time-Based Expiry (TTL) and Space-Based Capacity Eviction.

Expiry vs Eviction Comparison

DimensionTime-Based Expiry (TTL)Space-Based Capacity Eviction
Primary TriggerWall-clock elapsed time ($\Delta t > \text{TTL}$).RAM memory threshold (maxmemory reached).
Architectural GoalEnforce data freshness; prevent stale reads.Bound RAM memory footprint; prevent OOM crashes.
Operational TimingOccurs periodically or lazily upon key access.Occurs synchronously when a write fills RAM capacity.
Configurable ParametersKey TTL duration in seconds (EXPIRE key 300).Memory policies (allkeys-lru, volatile-lfu).

Core Eviction Algorithms: LRU, LFU, FIFO, and Random

When RAM reaches capacity, the eviction algorithm evaluates candidate keys to select a victim:

flowchart LR
  Policies[Cache Eviction Taxonomy]
  
  Policies --> LRU[LRU: Least Recently Used]
  Policies --> LFU[LFU: Least Frequently Used]
  Policies --> FIFO[FIFO: First In, First Out]
  Policies --> Random[Random Eviction]
  
  LRU --> LRUDesc[Evicts key untouched for longest time]
  LFU --> LFUDesc[Evicts key with lowest hit counter]
  FIFO --> FIFODesc[Evicts key with oldest creation timestamp]
  Random --> RandomDesc[Evicts a randomly selected key]

Figure 2: Taxonomy of major cache eviction algorithms.

1. LRU (Least Recently Used)

2. LFU (Least Frequently Used)

LFU Frequency Decay Mechanics

To solve Frequency Pollution in LFU, eviction engines implement **Frequency Decay (Ageing)**. A background timer or access step attenuates historical access counters over time. For example, every 10 minutes, all key access counters are halved ($C_{\text{new}} = \lfloor \frac{C_{\text{old}}}{2} \rfloor$). If a historical item receiving 1,000 hits is never accessed again, its counter decays to 500, 250, 125, and eventually 0, allowing newly popular items to enter RAM smoothly.

Redis Approximated LRU / LFU Implementation

To conserve memory and avoid maintaining a global doubly-linked list across millions of keys, Redis uses **Approximated LRU/LFU Algorithms**. Instead of tracking exact global ordering, Redis selects a random sample of $K$ keys (default $K=5$) when memory is full, and evicts the best victim within that 5-key sample. Increasing `maxmemory-samples` from 5 to 10 yields eviction behavior virtually indistinguishable from exact theoretical LRU while saving up to $30\%$ RAM per key.

The Scan Pollution Problem: Why LRU Fails During Sequential Scans

A major vulnerability of basic LRU is Scan Pollution (Sequential Scanning Vulnerability):

sequenceDiagram
    autonumber
    actor BatchJob as Nightly DB Backup / Batch Export
    participant Cache as LRU Cache (Capacity: 1,000 Items)
    
    Note over Cache: Cache contains 1,000 HOT product items (Hit Ratio: 98%)
    BatchJob->>Cache: Read 10,000 rare archival records sequentially...
    Note over Cache: LRU evicts all 1,000 HOT items to store 1,000 cold archival items!
    Note over Cache: RESULT: Hit Ratio collapses from 98% down to 0%! (Scan Pollution)

Figure 3: Sequence diagram demonstrating Scan Pollution wiping out hot items in LRU.

Modern Solutions: W-TinyLFU and ARC (Adaptive Replacement Cache)

To solve Scan Pollution, modern caching engines (such as Caffeine Cache or Redis 4.0+) deploy **W-TinyLFU** or **ARC**:

Data Structure Deep Dive: $O(1)$ LRU Implementation

To achieve $O(1)$ time complexity for both GET reads and PUT writes, LRU combines a Hash Map with a Doubly-Linked List:

flowchart LR
  subgraph HashMap for O1 Lookup
    Map["Hash Map Key -> Node Pointer"]
  end

subgraph Doubly-Linked List for O1 Ordering
Head[Head / Most Recently Used] <--> NodeA[Key: A]
NodeA <--> NodeB[Key: B]
NodeB <--> Tail[Tail / Least Recently Used]
end

Map -->|Pointer| NodeA
Map -->|Pointer| NodeB

Figure 4: $O(1)$ LRU data structure combining Hash Map pointers with a Doubly-Linked List.


Complete Worked Example: Go Thread-Safe $O(1)$ LRU Cache

Let's inspect a complete Go implementation of an $O(1)$ LRU Cache for the CacheLab platform (cachelab.com).

package main

import (
"container/list"
"fmt"
"sync"
)

type entry struct {
key string
value interface{}
}

type LRUCache struct {
mu sync.Mutex
capacity int
items map[string]list.Element
evictList
list.List
}

func NewLRUCache(capacity int) LRUCache {
return &LRUCache{
capacity: capacity,
items: make(map[string]
list.Element),
evictList: list.New(),
}
}

func (c *LRUCache) Get(key string) (interface{}, bool) {
c.mu.Lock()
defer c.mu.Unlock()

if elem, exists := c.items[key]; exists {
c.evictList.MoveToFront(elem) // Move accessed node to MRU head
return elem.Value.(*entry).value, true
}
return nil, false
}

func (c *LRUCache) Put(key string, value interface{}) {
c.mu.Lock()
defer c.mu.Unlock()

// Update existing key
if elem, exists := c.items[key]; exists {
c.evictList.MoveToFront(elem)
elem.Value.(*entry).value = value
return
}

// Evict LRU tail item if at capacity
if c.evictList.Len() >= c.capacity {
c.evictOldest()
}

// Insert new item at MRU head
ent := &entry{key: key, value: value}
elem := c.evictList.PushFront(ent)
c.items[key] = elem
}

func (c LRUCache) evictOldest() {
elem := c.evictList.Back() // Get LRU tail element
if elem != nil {
c.evictList.Remove(elem)
kv := elem.Value.(
entry)
delete(c.items, kv.key)
fmt.Printf("[EVICTION] Evicted LRU Key: %s\n", kv.key)
}
}


Failure Modes and Engineering Mitigations

Failure ScenarioRoot CauseSystem SymptomDetection SignalMitigation Strategy
1. LRU Scan Pollution CrashBatch analytics or backup job reads 1,000,000 rare keys sequentially into LRU cache.Cache Hit Ratio drops to 0%; database CPU surges to 100% as hot keys are evicted.Sudden drop in Cache Hit Ratio metrics accompanied by database read spikes.Switch eviction policy to W-TinyLFU or ARC to block admission of cold scan items.
2. LFU Frequency Accumulation LockHistorical event (e.g. Super Bowl) accumulates 1,000,000 hits on a key, which stays in RAM forever.Obsolete keys consume RAM permanently because recent items cannot match historical counters.High memory utilization with zero reads on top LFU key items.Deploy Frequency Ageing (Decay), periodically halving access counters over time.
3. Redis Approximated LRU VarianceRedis default maxmemory-policy volatile-lru uses 5-key random sampling instead of true LRU.Occasionally evicts slightly newer keys than true theoretical LRU would select.Minor unexpected cache misses on recent items under extreme memory saturation.Increase maxmemory-samples from 5 to 10 in Redis configuration for higher precision.
4. OOM Crashes from Missing Eviction CapsRedis configured with maxmemory-policy noeviction.Write operations fail with OOM command not allowed when RAM reaches 100%.Redis write error exceptions and failed application POST mutations.Always configure an active eviction policy (e.g. allkeys-lru or volatile-lfu).

What You Should Remember

  1. Eviction operates on capacity; Expiry operates on time: Expiry (TTL) enforces data freshness; Eviction manages finite RAM bounds.
  2. LRU exploits Temporal Locality: LRU evicts items untouched for the longest duration, executing in $O(1)$ time via HashMap + Doubly-Linked List.
  3. LFU evicts lowest total frequency: LFU keeps popular items in RAM, but requires frequency decay to prevent historical counter lockups.
  4. Use W-TinyLFU to prevent Scan Pollution: Probabilistic admission policies block single-use batch scan items from flushing hot keys out of RAM.
  5. Always specify an explicit maxmemory-policy: Avoid noeviction settings that crash writes when RAM reaches capacity.

Glossary of Terms

TermDefinition
Cache EvictionThe process of removing existing entries from an in-memory cache to free space for new entries when RAM is full.
LRU (Least Recently Used)An eviction policy that removes the item that has gone un-accessed for the longest duration.
LFU (Least Frequently Used)An eviction policy that removes the item with the lowest total access frequency counter.
Scan PollutionThe degradation of cache hit ratio caused by a sequential scan of cold data evicting hot cached items.
W-TinyLFUA modern eviction algorithm using Count-Min Sketch to enforce frequency-based admission control.
Doubly-Linked ListA data structure allowing $O(1)$ insertion, removal, and re-ordering of cache nodes.

Practice Scenario and Self-Assessment

Architecture Scenario

You are designing the in-memory cache for a social media newsfeed platform (`feed.cachelab.com`): **Questions**:
  1. Evaluate why basic LRU will suffer from Scan Pollution during nightly batch jobs.
  2. Formulate the configuration strategy (W-TinyLFU vs LFU with decay) to preserve viral newsfeed hit ratios.

Interactive Self-Assessment

The Hash Map provides O(1) key lookups, while the Doubly-Linked List allows O(1) node re-ordering and tail eviction without shifting arrays.

The Hash Map compresses persistent NVMe SSD disk sectors using Gzip compression.

The Doubly-Linked List automatically updates edge CDN TLS certificates.

Combining Hash Map and Doubly-Linked List doubles the physical hardware clock speed of CPU cores.

It uses a Count-Min Sketch frequency estimator to deny cache admission to low-frequency batch items, preserving hot keys in RAM.

It automatically converts relational database SQL schemas into un-indexed CSV files.

It forces client browsers to update their local hosts DNS configuration.

It downgrades the Linux operating system kernel version during batch jobs.


What to Learn Next

Track: Data, Storage and Messaging

Next: Cache Stampede — When Expiry Melts the Database

Series: Caching

  1. Caching 101 — Memory Offloading and Latency Reduction
  2. Caching Strategies — Aside, Through, Behind, and Refresh-Ahead
  3. Cache Eviction Policies — LRU, LFU, TTL, and Friends (this guide)
  4. Distributed Caching — Sharding and High-Availability Clusters
  5. Cache Stampede — When Expiry Melts the Database
  6. Stale Cache After Write — When Your Own Update Disappears
  7. Content Delivery Networks (CDN) — Edge Acceleration and Caching
  8. Bloom Filters — Probabilistic Set Membership at Scale

All series

By Shubham Jain

All articles · Study paths

Shubham Jain · Learning Lab