What Is the LRU Cache Eviction Policy?

What Is the LRU Cache Eviction Policy?

A beginner‑friendly, production‑grade guide to Least Recently Used caching — from the everyday intuition, through the classic HashMap + doubly linked list implementation, to how Redis, CPUs, and operating systems actually use it under load.

01

Introduction & History

Imagine your school bag. It can only hold a few books. Every day you bring new books for new subjects. If the bag is full and you need to add one more book, you must take one out. Which one do you remove? The smart answer is: remove the book you haven’t touched in the longest time — you probably won’t need it again soon.

That simple idea — throw away the thing you used longest ago — is exactly what the LRU (Least Recently Used) cache eviction policy does inside computers. LRU is a rule that decides what to remove from a cache when the cache is full and something new needs to come in.

A cache is a small, fast storage area that keeps copies of data so that future requests for that data can be served faster. Think of it as a “quick shelf” near your desk, versus the “big warehouse” (the main database or disk) far away. Grabbing something from the quick shelf is much faster than walking to the warehouse.

1.1 Why does LRU exist?

Caches are always small compared to the total amount of data that exists. Your laptop’s memory is small compared to the internet. A web server’s cache is small compared to its entire database. Because a cache is limited in size, at some point it becomes full. When that happens, the system must decide: which existing item should I throw away to make room for the new one? That decision‑making rule is called an eviction policy, and LRU is the most famous and widely used one.

1.2 A short history

  • 1960sVirtual memory research. Early mainframes had tiny, expensive main memory and needed strategies to decide which pages of a program to keep in memory versus which to write back to slower disk. Researchers like László Bélády formalised the study of “page replacement,” giving us FIFO, LRU, and the theoretical Optimal (Bélády’s) algorithm.
  • 1970s–80sOperating systems adopt LRU. Unix‑family kernels and their descendants ship with LRU‑style page replacement (often the “Clock” or “Second‑Chance” approximation) as the default way of choosing which memory pages to swap out.
  • 1990sCPU caches and databases. Modern processors bake pseudo‑LRU logic directly into L1/L2/L3 cache hardware. Databases like Oracle and later MySQL InnoDB start using LRU‑based buffer pools to keep hot data pages in RAM.
  • 2000sWeb caches & CDNs. Squid, Varnish, and CDNs (Akamai, Cloudflare) adopt LRU‑style eviction to keep the most‑requested URLs cached close to users.
  • 2010sDistributed key‑value caches. Redis and Memcached make LRU (and, for Redis, its approximate‑LRU sampling) the standard eviction policy of large‑scale application caches at companies like Twitter, GitHub, and Uber.
  • 2020sIn‑process caches & smart variants. Libraries such as Caffeine bring near‑optimal, high‑concurrency LRU‑family algorithms (TinyLFU/W‑TinyLFU) directly inside Java processes, and the field continues to refine variants like ARC, 2Q, and SLRU.

Today, LRU (or a variant of it) sits quietly inside almost every layer of modern software, from the chip in your phone to the servers behind Netflix and Amazon — often without any developer having to name it explicitly.

i
Simple analogy

A cache is like the small whiteboard on your fridge where you jot down things you need to remember right now (like “buy milk”). Your full memory (brain) is the big database. The whiteboard is small, so when it fills up, you erase the note you wrote longest ago to make space for a new one. That’s LRU in daily life.

1.3 Where you meet LRU without even knowing it

You interact with LRU‑style thinking many times a day, even outside computers. Your phone’s recent‑apps switcher shows the apps you touched most recently first, and the ones you haven’t opened in weeks slowly fall off the list. A physical library with limited shelf space near the front desk might keep the most recently returned books there, sending older, untouched books to storage in the back. Your own short‑term memory works similarly — you remember the last few things someone told you clearly, while details from a conversation days ago start to fade unless you kept “using” (recalling) them.

Computer scientists noticed this same pattern was incredibly useful for managing limited, fast storage, and turned it into a formal, precise algorithm that a machine could execute perfectly, every single time, in a fraction of a millisecond.

02

The Problem & Motivation

Let’s understand the exact problem LRU solves, step by step, with no assumed knowledge.

2.1 Why do we even need a cache?

Fetching data from a hard disk, a database, or across the internet takes time — often measured in milliseconds. Fetching data from memory that is already close to the processor takes far less time — often measured in nanoseconds or microseconds. If your application repeatedly asks for the same piece of data (say, a popular product page on an e‑commerce site), it is wasteful to go all the way to the database every single time. Instead, we keep a copy of frequently‑requested data in a fast cache.

Storage typeTypical speedAnalogy
CPU register~1 nanosecondItem in your hand
CPU cache (L1/L2/L3)~1–10 nanosecondsItem on your desk
RAM (main memory)~100 nanosecondsItem in your room
SSD disk~0.1 millisecondItem in your garage
Network / remote database~1–100+ millisecondsItem in a warehouse across town

2.2 Why can’t the cache just hold everything?

Fast memory is expensive and physically limited. You cannot build a cache the same size as your entire database — that would defeat the purpose (and cost a fortune). So caches are deliberately small, which means they will eventually fill up.

2.3 What happens when the cache is full?

When a full cache needs to store one more new item, it must remove something old first. This removal process is called eviction. The big question is: which item should be evicted? This is where an eviction policy comes in. Choosing badly means we throw away data we will need again very soon, causing a “cache miss” and a slow trip to the database. Choosing wisely means we keep the data we’re most likely to need, and cache misses become rare.

A good eviction policy is really a prediction: predicting what you will need next, based on what has happened so far.

2.4 Why “Least Recently Used” specifically?

LRU is based on a real‑world observation called temporal locality — if a piece of data was accessed recently, it’s likely to be accessed again soon. Think about the last five apps you opened on your phone. There’s a good chance your very next app will be one of those five, not some app you haven’t touched in six months. LRU turns this everyday pattern into a precise computer algorithm: always keep the recently‑touched items, always throw away the item that has been sitting untouched the longest.

2.5 What would happen without any eviction policy at all?

If a system simply kept adding new items to a full cache without ever removing anything, one of two bad things would happen. Either the cache would keep growing forever, eventually consuming all available memory and crashing the application (a memory leak), or the system would have to reject every new item once the cache was full, meaning the cache could never store anything new — including data that might be far more useful than what’s already sitting there. Neither option is acceptable for a real system, which is exactly why a deliberate, well‑thought‑out eviction policy like LRU is necessary.

2.6 The theoretical “best possible” policy — and why it’s impossible

Interestingly, computer science has already identified the mathematically optimal eviction policy: it’s called Bélády’s algorithm, or the “Optimal Replacement Policy.” Its rule is simple to state: always evict the item that will not be needed again for the longest time in the future. The catch is that this requires knowing the future — which item will be requested next, and when — something no real system can know in advance. Bélády’s algorithm is mainly used as a theoretical benchmark, to measure how close a practical policy like LRU comes to perfection. LRU is popular precisely because it approximates this unreachable ideal using only information about the past, which is the only information any real system actually has.

03

Core Concepts

Before going deeper, let’s clearly define every term we will use throughout this tutorial.

Cache

DefA small, fast storage layer that holds copies of frequently or recently used data, so future requests are faster.

Cache Hit

DefWhen the requested data is found inside the cache. Fast and cheap.

Cache Miss

DefWhen the requested data is NOT in the cache, forcing a slower fetch from the main data source.

Cache Capacity

DefThe maximum number of items (or bytes) the cache can hold before it must evict something.

Eviction Policy

DefThe rule used to decide which item to remove when the cache is full and needs space for a new item.

LRU (Least Recently Used)

DefAn eviction policy that removes the item that has not been accessed for the longest time.

Recency

DefHow recently an item was accessed. LRU cares only about recency, not how many times an item was used.

get() operation

DefRead an item from the cache. In LRU, a successful get() also marks that item as “most recently used.”

put() operation

DefInsert or update an item in the cache. If the cache is full, this may trigger an eviction first.

3.1 Beginner example

Say your cache can hold only 3 items, and you access pages in this order: A, B, C, A, D.

  1. Access A → cache: [A] (A is most recent)
  2. Access B → cache: [A, B] (B is most recent)
  3. Access C → cache: [A, B, C] (full now, C is most recent)
  4. Access A again → A moves to “most recent.” Cache order (least to most recent): [B, C, A]
  5. Access D → cache is full. Evict the least recently used item, which is B. Cache becomes: [C, A, D]

Notice that B was evicted — not C — even though B was added before C, because A was re‑accessed and “refreshed” its recency, but B’s recency never got refreshed and it became the oldest.

!
Common misunderstanding

LRU is NOT about how many times an item was used (that’s a different policy called LFU — Least Frequently Used). LRU only cares about when an item was last touched, never how many times.

3.2 Software example

Imagine an online food delivery app. Every time a user opens the app, it needs to show the menu of a nearby restaurant. Fetching a restaurant’s full menu from the database takes, say, 40 milliseconds — noticeable to a human. If the app keeps the last 500 viewed restaurant menus in an LRU cache, then whenever someone reopens a popular restaurant’s page (which happens constantly during lunch hours), the response comes back almost instantly from the cache instead of hitting the database again. As new restaurants get viewed and the cache fills up, the menus nobody has looked at recently quietly get evicted to make room — exactly the behavior you want, since a restaurant nobody is currently browsing is unlikely to be requested again in the next few seconds.

3.3 Production example: Amazon‑style product pages

A large e‑commerce platform might serve millions of product page views per minute. Only a small fraction of the total product catalog — the trending items, deals, and popular categories — accounts for the vast majority of traffic at any given moment (this is sometimes called the “80/20 rule” or a Zipfian distribution). An LRU cache is a natural fit here: the currently trending products stay “hot” in the cache because they keep getting re‑accessed, refreshing their position, while yesterday’s trending products that nobody is looking at anymore naturally age out and get evicted, freeing space for today’s new trends — all without anyone having to manually configure which products should be cached.

04

Architecture & Components

To make LRU fast in real software, we need two things working together:

  1. A HashMap (or hash table) — for instant lookup: “does this key exist in the cache, and where is it?”
  2. A Doubly Linked List — for keeping track of usage order, so we always know which item is least recently used (at the tail) and which is most recently used (at the head).

Why do we need both? A HashMap alone gives us fast lookup (O(1)) but has no idea about order of use. A linked list alone can track order, but finding a specific item inside it would require scanning through it (slow, O(n)). By combining both, we get the best of each: instant lookup and instant reordering.

HashMap + Doubly Linked List = O(1) LRU HASHMAP (key → node) key: A → node key: B → node key: C → node O(1) lookup by key no scanning required DOUBLY LINKED LIST (usage order) HEAD MRU end Node B prev ↔ next Node C prev ↔ next Node A prev ↔ next TAIL LRU end most recent evict here unlink + re-insert = O(1) reorder evict tail = O(1) removal

Fig 4.1 — The HashMap gives O(1) access to any node. The Doubly Linked List keeps nodes ordered from most‑recently‑used (head) to least‑recently‑used (tail), so eviction always removes the tail node in O(1) time.

4.1 Why a doubly linked list, not a singly linked list?

In a singly linked list, each node only points forward to the next node. To remove a node from the middle (which happens every time we re‑access an item), we’d need to know its previous node too — and finding that would require scanning the whole list. A doubly linked list stores both “next” and “previous” pointers on every node, so we can unlink any node instantly, without searching, as long as we already have a direct reference to it (which the HashMap gives us).

HashMap responsibility

Maps each key to the exact memory location (node) inside the linked list, so we never scan the list to find an item.

Linked list responsibility

Maintains strict order of use. The node at the front is freshest; the node at the back is the eviction candidate.

Capacity counter

Tracks current size vs. maximum allowed size, to know when eviction is required.

05

Internal Working

Let’s walk through exactly what happens, in plain steps, for the two core operations.

5.1 get(key) — reading data

Look up the key in the HashMap

If the key doesn’t exist, it’s a cache miss — return “not found” (or fetch from the real data source and then insert it, depending on the caching strategy).

If found, get the node

The HashMap gives us direct access to the node in the linked list — no scanning needed.

Move the node to the head

Unlink it from its current position (using its previous/next pointers) and re‑attach it right after the head, marking it as most recently used.

Return the value

The caller receives the cached data — no need to touch the slow database.

5.2 put(key, value) — writing data

Check if the key already exists

If it does, update its value, then move that node to the head (same as a get).

If the key is new and cache has room

Create a new node, insert it at the head, and add the key → node mapping into the HashMap.

If the key is new and cache is full

First evict the tail node (least recently used): remove it from the linked list and delete its key from the HashMap. Then insert the new node at the head as usual.

i
Why every step is O(1)

HashMap lookup is O(1) on average. Removing or inserting a node next to a known position in a doubly linked list is O(1), because we directly rewire a few pointers — we never have to search or shift elements like we would in an array.

06

Data Flow & Lifecycle

Here is a sequence diagram showing a realistic request flow through an LRU cache sitting in front of a database — this is the most common real‑world pattern, called cache‑aside.

Cache-Aside Pattern with an LRU Cache Application LRU Cache Database get(“user:101”) alt — CACHE HIT return cached value move “user:101” to HEAD (MRU) else — CACHE MISS not found SELECT * FROM users WHERE id=101 row: user data put(“user:101”, data) insert at HEAD, evict TAIL if full

Fig 6.1 — The cache‑aside pattern. The application always asks the cache first. Only on a miss does it pay the cost of a database round trip, and then it stores the fresh result in the cache for next time.

6.1 A full lifecycle example, step by step

Let’s trace a cache with capacity 3 through a sequence of operations, showing the internal state after each step.

OperationCache state (MRU → LRU)Notes
put(1, “A”)[1]Cache was empty, item inserted
put(2, “B”)[2, 1]New head is 2
put(3, “C”)[3, 2, 1]Cache is now full (capacity 3)
get(1)[1, 3, 2]1 was accessed, moves to head
put(4, “D”)[4, 1, 3]Cache full → evict LRU item (2)
get(2)[4, 1, 3]Miss — 2 was evicted earlier
07

Java Implementation

There are two common ways to implement LRU in Java. Let’s look at both.

7.1 Option A: The quick way — using LinkedHashMap

Java’s built‑in LinkedHashMap can maintain access order and has a built‑in hook for eviction, making it perfect for a simple, production‑ready LRU cache in just a few lines.

LRUCache.java — LinkedHashMap approach
import java.util.LinkedHashMap;
import java.util.Map;

public class LRUCache<K, V> extends LinkedHashMap<K, V> {

    private final int capacity;

    public LRUCache(int capacity) {
        // initialCapacity, loadFactor, accessOrder=true (tracks usage order)
        super(capacity, 0.75f, true);
        this.capacity = capacity;
    }

    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        // Called automatically after every put(); returning true evicts the oldest entry
        return size() > capacity;
    }

    public static void main(String[] args) {
        LRUCache<Integer, String> cache = new LRUCache<>(3);
        cache.put(1, "A");
        cache.put(2, "B");
        cache.put(3, "C");
        cache.get(1);          // 1 becomes most recently used
        cache.put(4, "D");     // evicts 2 (least recently used)

        System.out.println(cache.keySet()); // [3, 1, 4]
    }
}

This works because accessOrder=true tells LinkedHashMap to reorder entries every time they are read, and removeEldestEntry is a hook Java calls after every insert, letting us evict automatically.

7.2 Option B: The “from scratch” way — HashMap + Doubly Linked List

This is the version most interview and system‑design discussions expect, because it shows you understand exactly how LRU works internally rather than relying on a library.

LRUCacheManual.java — HashMap + doubly linked list
import java.util.HashMap;

public class LRUCacheManual {

    class Node {
        int key, value;
        Node prev, next;
        Node(int key, int value) {
            this.key = key;
            this.value = value;
        }
    }

    private final int capacity;
    private final HashMap<Integer, Node> map;
    private final Node head; // dummy head (most recently used side)
    private final Node tail; // dummy tail (least recently used side)

    public LRUCacheManual(int capacity) {
        this.capacity = capacity;
        this.map = new HashMap<>();
        head = new Node(0, 0);
        tail = new Node(0, 0);
        head.next = tail;
        tail.prev = head;
    }

    private void remove(Node node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }

    private void insertAtHead(Node node) {
        node.next = head.next;
        node.prev = head;
        head.next.prev = node;
        head.next = node;
    }

    public int get(int key) {
        if (!map.containsKey(key)) return -1;
        Node node = map.get(key);
        remove(node);
        insertAtHead(node);
        return node.value;
    }

    public void put(int key, int value) {
        if (map.containsKey(key)) {
            remove(map.get(key));
        }
        Node node = new Node(key, value);
        insertAtHead(node);
        map.put(key, node);

        if (map.size() > capacity) {
            Node lru = tail.prev;      // node just before dummy tail
            remove(lru);
            map.remove(lru.key);
        }
    }
}

Notice the two dummy (sentinel) nodes, head and tail. They exist purely to avoid messy null checks when inserting into or removing from an empty or single‑item list — a very common, clean production trick.

When to use LinkedHashMap

  • Fast to write and easy to maintain
  • Great for real applications and prototypes
  • Fewer bugs since Java handles the linked‑list logic

When to use manual HashMap + DLL

  • Interviews and system design rounds
  • When you need full control (e.g., custom eviction hooks, TTL, metrics)
  • When building your own caching library
08

Advantages, Disadvantages & Trade‑offs

Advantages

  • O(1) time complexity for both get and put
  • Simple to reason about and implement correctly
  • Matches real‑world access patterns (temporal locality) very well
  • Widely supported: built into Redis, Linux, databases, and standard libraries
  • Predictable behavior, easy to test and debug

Disadvantages

  • Vulnerable to “cache pollution” from one‑time scans (e.g., a big batch job can flush out useful data)
  • Ignores frequency — an item used 1000 times can be evicted just because a random new item was touched once, more recently
  • Extra memory overhead for pointers (prev/next) per entry
  • Not ideal for workloads with cyclic access patterns larger than the cache size
!
The classic LRU weakness: “cache pollution”

Imagine a cache full of your most useful, frequently accessed data. Then a report‑generation job runs once and reads through ten thousand records, each accessed exactly one time. Because LRU only cares about recency, all of that one‑time data pushes out your genuinely important, frequently‑used data. This is one reason engineers sometimes prefer LFU or hybrid policies like ARC or LRU‑K for such workloads.

09

LRU vs Other Eviction Policies

LRU is just one of several eviction strategies. Here’s how it compares to the other common ones.

PolicyRuleBest forWeakness
LRUEvict the item unused for the longest timeGeneral‑purpose, recency‑heavy workloadsIgnores frequency; hurt by one‑time scans
LFU (Least Frequently Used)Evict the item used the fewest number of timesStable “hot item” workloadsSlow to adapt to new trending items; old popular items can get “stuck” forever
FIFO (First In, First Out)Evict the oldest inserted item, regardless of useVery simple systems, low overheadIgnores usage entirely — can evict a hot item
MRU (Most Recently Used)Evict the item used most recentlyRare cases like repeated full‑table scansCounter‑intuitive for most workloads
RandomEvict a randomly chosen itemExtremely simple systems, low CPU costNo intelligence, unpredictable performance
ARC (Adaptive Replacement Cache)Dynamically balances recency and frequencyMixed, changing workloadsMore complex to implement
One Trigger, Four Different Answers Cache is full — new item arrives something must be evicted Which policy? apply eviction rule LRU evict unused longest this tutorial LFU evict used fewest times FIFO evict oldest inserted Random evict any item

Fig 9.1 — Different eviction policies branch off from the same trigger — a full cache — but choose the victim using different logic.

10

Performance & Scalability

10.1 Time complexity

O(1)get()
O(1)put()
O(1)eviction
O(n)space (n = capacity)

These constant‑time guarantees are exactly why LRU is chosen over naive approaches like scanning an array of “last used timestamps,” which would cost O(n) per operation and quickly become a bottleneck under heavy load.

10.2 Scaling LRU in real systems

  • Sharding: Instead of one giant cache, split it into many smaller caches (shards) by key, each with its own LRU list. This reduces lock contention and lets shards run on different machines.
  • Sampling‑based approximate LRU: True LRU under massive concurrent load can create contention on the linked list. Systems like Redis use an approximate LRU — sampling a handful of random keys and evicting the “oldest” among that sample, which is nearly as effective but far cheaper at scale.
  • Segmented / tiered caches: Combine a small, ultra‑fast in‑process cache (like Caffeine in Java) with a larger, shared distributed cache (like Redis) behind it, each with its own LRU eviction.
i
Why Redis uses approximate LRU

A perfectly precise LRU list across millions of keys, updated on every single read, would need heavy synchronization and memory overhead. Redis instead keeps a rough “last accessed” timestamp per key and, when it needs to evict, samples a small set of keys (default 5) and evicts the oldest among them. This trades a small amount of precision for a big gain in speed and simplicity — a classic real‑world engineering trade‑off.

10.3 Why not just use a simple array with timestamps?

A beginner’s first instinct is often: “why not just store a ‘last accessed time’ next to every item in an array, and scan the array to find the oldest one when eviction is needed?” This does work correctly, but it doesn’t scale. Scanning an array of size n to find the minimum timestamp takes O(n) time — for every single eviction. With a cache holding a million items, that’s a million comparisons on every eviction, which quickly becomes far too slow for a system handling thousands of requests per second. The HashMap plus doubly‑linked‑list design exists specifically to avoid this scan, replacing it with a small, constant number of pointer operations regardless of how large the cache is. This is a great example of a general lesson in software engineering: the right combination of data structures can turn a slow O(n) operation into a fast O(1) one.

10.4 Benchmarking mindset

When evaluating or tuning any real cache, engineers typically look at three things together, never just one in isolation: the hit rate (is the cache actually useful?), the latency of cache operations themselves (is the cache fast enough to be worth using?), and the cost of the memory it consumes (is the cache size justified by the benefit?). A cache with a 99% hit rate that is itself slow, or a cache that is blazing fast but rarely has what’s needed, are both signs that something needs tuning — either the eviction policy, the cache size, or what’s being cached in the first place.

11

Concurrency & Thread Safety

In real applications, many threads (or many servers) may read and write to the cache at the same time. A naive LRU implementation is not thread‑safe by default, because two threads could try to modify the linked list pointers simultaneously and corrupt it.

11.1 Common solutions

Synchronized wrapper

Wrap get/put in a lock (e.g., synchronized in Java). Simple but can become a bottleneck under high concurrency.

Read‑write locks

Allow multiple concurrent reads but exclusive writes — better throughput than a single lock when reads dominate.

Lock striping / sharded caches

Split the cache into multiple independent segments, each with its own lock, so unrelated keys don’t block each other.

Lock‑free / concurrent structures

Libraries like Caffeine (Java) use highly tuned concurrent data structures and batched, asynchronous bookkeeping to avoid locking on the hot path entirely.

!
A subtle bug to watch for

Even a “thread‑safe” HashMap like Java’s ConcurrentHashMap does not automatically make your LRU linked‑list reordering safe. You still need to protect the combined HashMap + linked‑list structure as one unit, because a get() that reorders the list is really a write operation, not just a read.

12

Distributed Caching & High Availability

Large applications don’t run an LRU cache on just one machine — they run a distributed cache spread across many servers, often using systems like Redis or Memcached.

12.1 Partitioning (sharding)

Keys are split across multiple cache nodes using a hashing strategy (often consistent hashing), so each node only manages a portion of the keyspace and runs its own local LRU eviction independently.

12.2 Replication

To avoid losing cached data (or, more importantly, to avoid overwhelming the database) if a cache node crashes, data is often replicated to one or more secondary nodes. If the primary node fails, a replica can take over.

Distributed LRU: Sharded Cache with Replicas Application many app servers issue get/put Cache Client / Router consistent hashing key → shard Cache Node 1 LRU (own list) Cache Node 2 LRU (own list) Cache Node 3 LRU (own list) Replica 1 Replica 2 Replica 3 replicate replicate replicate each shard runs its own LRU eviction — failover to replica on crash

Fig 12.1 — A typical distributed LRU cache cluster. Keys are routed to the correct shard, and each shard has its own replica for failover.

12.3 CAP theorem and caching

The CAP theorem says a distributed system can only fully guarantee two out of three: Consistency, Availability, and Partition tolerance. Since network partitions can always happen, real systems must choose between consistency and availability during a partition. Most caching systems lean toward availability — it’s usually fine to serve slightly stale cached data for a moment rather than fail the request entirely, since a cache is a performance optimization, not the source of truth.

12.4 Failure recovery

  • Cache warm‑up: After a crash or restart, an empty cache causes a sudden wave of cache misses (called a “cold cache” or “thundering herd”). Systems often pre‑load (“warm”) the cache with the most important keys before routing real traffic to it.
  • Graceful degradation: If the cache cluster is unreachable, well‑designed applications fall back to querying the database directly rather than crashing.
  • Circuit breakers: Prevent an overloaded or failing cache layer from taking down the whole application by “tripping” and temporarily bypassing the cache.

12.5 Consensus and coordination

In more advanced distributed caching setups, nodes need to agree on things like “who is the current primary for this shard” or “has this replica finished syncing.” This is where consensus protocols (such as Raft, used internally by systems like Redis Sentinel and many cloud‑managed cache services) come in — they help a cluster of machines agree on a single, consistent view of cluster state even when some nodes are slow or temporarily unreachable. You don’t need to implement these yourself when using managed services like AWS ElastiCache or Redis Enterprise, but understanding that this coordination is happening underneath is valuable for reasoning about failure scenarios.

12.6 Consistent hashing, explained simply

When you have multiple cache nodes, you need a way to decide which node stores which key. A naive approach — node_number = hash(key) % number_of_nodes — has a big problem: if you add or remove even a single node, almost every key suddenly maps to a different node, causing a massive, unnecessary wave of cache misses. Consistent hashing solves this by arranging nodes and keys on a conceptual circle (a “hash ring”). Adding or removing a node then only affects the small portion of keys near that node on the ring, leaving the vast majority of key‑to‑node mappings undisturbed. This is why consistent hashing is the standard technique behind virtually every large‑scale distributed cache and load balancer.

13

Security Considerations

Sensitive data exposure

Caches can accidentally store sensitive information (passwords, tokens, personal data) longer than intended. Always define what is safe to cache.

Cache poisoning

An attacker tricks the system into caching malicious or incorrect data, which then gets served to other users. Validate and sanitize any data before caching it.

Denial‑of‑service via eviction abuse

An attacker floods the cache with many unique, junk keys, forcing constant eviction of legitimate data (“cache thrashing”). Rate limiting and key validation help prevent this.

Access control

A shared cache used by multiple tenants or users must ensure one user cannot read another user’s cached data — use properly scoped cache keys (e.g., prefixing with a user ID).

!
Real‑world reminder

Never cache raw secrets like passwords or full credit card numbers. Even a fast, well‑designed LRU cache is still a place data lives in memory, and memory can be inspected, dumped, or exposed through bugs. Cache only what’s safe to expose if something goes wrong.

13.1 Encryption and network security

When a cache like Redis is deployed as a separate networked service, the connection between the application and the cache should be encrypted (using TLS) and authenticated (using passwords or access control lists), especially in cloud environments where network traffic could otherwise be intercepted by anything else running on a shared network. Cloud providers’ managed cache services generally offer these protections as configuration options — but they are not always turned on by default, so it’s worth explicitly verifying them during setup rather than assuming they’re active.

14

Monitoring, Logging & Metrics

You cannot improve what you don’t measure. Here are the key metrics engineers track for any LRU‑based cache in production.

MetricWhat it tells you
Hit ratePercentage of requests served directly from cache. Higher is better — a common healthy target is 80–95%+, depending on the workload.
Miss ratePercentage of requests that had to go to the database. Sudden spikes often signal cache pollution or under‑sizing.
Eviction rateHow often items are being evicted. Very high eviction rates suggest the cache is too small for the workload.
Latency (p50/p95/p99)Response time distribution for cache operations — spikes can reveal lock contention or resource pressure.
Memory usageHow close the cache is to its configured memory limit.
Key size distributionHelps detect abnormally large values that could distort memory usage and force excessive eviction.

Tools like Prometheus + Grafana, Datadog, and Redis’s own INFO command (which reports keyspace_hits, keyspace_misses, and evicted_keys) are commonly used to track these metrics in real time and trigger alerts when hit rate drops or eviction spikes.

14.1 Setting up meaningful alerts

Rather than alerting on every small fluctuation, mature teams typically set alerts on sustained trends: for example, “hit rate has stayed below 70% for more than 10 minutes” or “eviction rate has tripled compared to the same time yesterday.” These kinds of comparative, trend‑based alerts catch real problems — like a sudden traffic pattern change, a misconfigured cache size after a deployment, or a bug that’s generating too many unique cache keys — without paging an engineer for normal, healthy variation. Logging cache key patterns (without logging sensitive values) also helps engineers debug unexpected pollution, such as noticing that a new feature is accidentally generating a unique cache key per user session instead of reusing a shared key, silently flooding the cache with one‑time entries.

15

Deployment & Cloud

LRU eviction shows up across many layers of a real production stack, not just as a standalone cache server.

In‑process cache

Libraries like Caffeine or Guava Cache (Java) run inside the same process as the application, using LRU or LRU‑like eviction, for the fastest possible access with no network hop.

Distributed cache

Redis and Memcached run as separate services (often managed via AWS ElastiCache, Google Cloud Memorystore, or Azure Cache for Redis) and support LRU‑based maxmemory-policy settings.

CDN edge caching

Content Delivery Networks (like Cloudflare, Akamai, or CloudFront) cache static assets at edge locations close to users, using recency/frequency‑based eviction to decide what stays cached at each edge node.

Database buffer pools

Databases like MySQL (InnoDB) and PostgreSQL keep frequently used data pages in memory using an LRU‑based (often modified) buffer pool eviction strategy.

15.1 Cost optimization

Since fast memory (RAM) costs more than disk, cache sizing is a direct cost decision. Engineers monitor hit rate versus memory cost, and often find a “sweet spot” cache size beyond which adding more memory yields diminishing improvements in hit rate — a classic cost‑performance trade‑off in cloud infrastructure.

16

Design Patterns & Anti‑patterns

16.1 Common caching patterns that use LRU underneath

Cache‑Aside (Lazy Loading)

The application checks the cache first; on a miss, it loads from the database and writes the result into the cache. Most common pattern.

Write‑Through

Every write goes to the cache and the database at the same time, keeping them always in sync, at the cost of slightly slower writes.

Write‑Behind (Write‑Back)

Writes go to the cache immediately and are asynchronously flushed to the database later — faster writes, but a small risk of data loss on crash.

Read‑Through

The cache itself is responsible for fetching missing data from the database, hiding that logic from the application.

16.2 Anti‑patterns to avoid

Anti‑patterns

  • Caching everything blindly — wastes memory on data that’s rarely reused
  • No expiration (TTL) — stale data can live forever if only relying on LRU eviction
  • Using a huge single global cache with one lock — creates contention under load
  • Caching highly volatile data — data that changes every second gains little from caching and risks serving stale results
  • Ignoring cache stampede risk — many requests missing the cache at the same instant and all hammering the database simultaneously

Better approaches

  • Combine LRU with a Time‑To‑Live (TTL) expiration
  • Use sharded caches to reduce lock contention
  • Cache only data with genuine reuse potential
  • Use request coalescing or locks to prevent cache stampedes
17

Best Practices & Common Mistakes

17.1 Best practices

  • Choose a cache size based on real traffic analysis, not guesswork — measure hit rate before and after resizing.
  • Combine LRU eviction with a TTL so stale data eventually expires even if it stays “recently used.”
  • Use meaningful, namespaced cache keys (e.g., user:101:profile) to avoid collisions and simplify invalidation.
  • Monitor hit rate continuously — a dropping hit rate is an early warning sign of a problem.
  • Prefer proven libraries (Caffeine, Redis) over hand‑rolled caches in production unless you have a very specific need.

17.2 Common mistakes (with fixes)

Mistake — forgetting to reorder on read

The get() path doesn’t move the node to the head, so recency stops being tracked. The cache silently degrades into FIFO behavior and its hit rate mysteriously drops.

Fix — treat get() as a write

Every successful read unlinks the node and re‑inserts it at the head, or delegates to a battle‑tested library where this is guaranteed.

Mistake — unhandled edge capacities

Capacity of zero or one breaks a naive implementation (double free, null pointer, or off‑by‑one) because sentinels and boundary checks were only tested with capacity of 3+.

Fix — explicit tests at 0, 1, N and N+1

Add unit tests for empty caches, single‑slot caches, exactly‑full caches, and one‑over‑full caches. Use sentinel head/tail nodes so pointer wiring never sees null.

Mistake — using the cache as a database

Product code writes something into the cache and reads it back later expecting durability. On restart or eviction that data is gone, and users hit “record not found” errors.

Fix — cache is a copy, never the source of truth

All writes go to the durable store first (database, event log), and the cache is populated as a derived copy. Cache misses are always safe to serve from the source.

Mistake — ignoring memory limits

Capacity is set by item count only. Some values are 100 bytes, others are 5 MB, so the cache silently grows past the RAM budget and either swaps or crashes.

Fix — size by both count and bytes

Use a weight‑aware eviction (Caffeine’s maximumWeight, Redis’s maxmemory) so a few huge values can’t squeeze out the rest of the cache.

Mistake — thread‑unsafe cache under load

Multiple threads share a plain HashMap + linked list. Under production concurrency, pointers get corrupted, iteration throws, and rare crashes show up only at peak traffic.

Fix — protect the whole structure as one unit

Use a synchronized wrapper, a lock‑striped design, or a proven concurrent library (Caffeine) so the HashMap and the linked list update atomically together.

17.3 A simple approach to capacity planning

A practical way to size an LRU cache is to start by estimating your “working set” — the set of items your application realistically needs at once during normal peak traffic. For example, if your analytics show that 95% of requests over the last week touched only 10,000 unique product IDs, then a cache sized to comfortably hold those 10,000 items (plus a safety margin) is a reasonable starting point. From there, watch the hit rate in production: if it’s consistently above roughly 90% and eviction rate is low, your sizing is likely healthy. If hit rate is low and eviction is constant, either your cache is too small for the workload, or your workload has very low temporal locality and might not benefit much from LRU caching at all — both are useful, actionable signals rather than guesses.

18

Real‑World & Industry Examples

CPU caches

Modern processors use LRU or pseudo‑LRU policies inside L1/L2/L3 caches to decide which cache lines to evict, keeping frequently used instructions and data close to the CPU core.

Operating systems

Linux and other OS kernels use LRU‑based (often “Clock” or “Second‑Chance,” an efficient approximation of LRU) page replacement algorithms to decide which memory pages to swap out to disk.

Redis

Redis supports several maxmemory-policy options including allkeys-lru and volatile-lru, using approximate LRU sampling for speed at scale, powering caching layers at companies like Twitter, GitHub, and Uber.

Netflix

Netflix relies heavily on distributed caching (including EVCache, built on Memcached) with LRU‑style eviction to serve personalized recommendations and metadata to millions of users with very low latency.

Web browsers

Browsers like Chrome cache recently visited pages, images, and scripts locally, using recency‑based eviction so your back button and repeat visits load instantly.

Databases

MySQL’s InnoDB buffer pool and PostgreSQL’s shared buffers use LRU‑based (with tweaks to avoid pollution from large scans) strategies to keep hot data pages in memory.

Content Delivery Networks

CDNs like Cloudflare and Akamai cache website assets (images, videos, scripts) at edge servers physically close to users worldwide. LRU‑style eviction ensures each edge location keeps the content that region’s users actually watch or view, freeing space from content nobody nearby is requesting anymore.

Uber

Uber’s backend systems cache frequently requested data such as driver locations, pricing estimates, and map tiles using distributed caches with recency‑based eviction, allowing the app to respond in real time even while handling enormous request volumes during peak hours like rush hour or major events.

Notice a pattern across all these examples: whether it’s a CPU chip the size of a fingernail or a global network of thousands of servers, the underlying challenge is identical — a small, fast storage space, a much larger pool of possible data, and a need to intelligently decide what stays and what goes. LRU (or one of its refined variants) is one of the most trusted answers to that challenge across the entire computing industry.

19

Advanced Topics & LRU Variants

Because plain LRU has known weaknesses (like vulnerability to one‑time scans), several improved variants have been developed over the years.

VariantIdea
LRU‑KTracks the last K accesses instead of just the most recent one, making it much more resistant to being fooled by a single one‑time access.
2QUses two queues — one for items seen once, one for items seen more than once — so single‑use scans don’t pollute the “proven useful” queue.
SLRU (Segmented LRU)Splits the cache into a “probationary” segment and a “protected” segment; items must be accessed twice to move into the protected (safer) segment.
ARC (Adaptive Replacement Cache)Dynamically balances between recency and frequency based on observed workload, self‑tuning over time. Used in ZFS filesystem caching.
Clock / Second‑ChanceAn efficient approximation of LRU using a circular buffer and a single “reference bit” per item, avoiding the overhead of a full linked list — common in OS page replacement.
Segmented LRU: Items Must Earn Their Way In New Item unknown, unproven Probationary Segment accepts every arrival short-lived if unused Protected Segment proven items only safer from eviction accessed again coldest protected item demoted back Evicted — one-time scan traffic

Fig 19.1 — Segmented LRU (SLRU). New items start on probation. Only items proven useful by a second access earn a spot in the safer, protected segment — reducing pollution from one‑time scans.

i
Interview‑relevant insight

If you’re ever asked “how would you improve plain LRU,” a strong answer is: mention that plain LRU is vulnerable to scan pollution, and that LRU‑K, 2Q, or ARC solve this by requiring repeated access before granting an item strong protection from eviction.

20

Frequently Asked Questions

Is LRU always the best eviction policy?

No. LRU is a strong general‑purpose default, but LFU, ARC, or LRU‑K variants can outperform it for specific workloads, especially those with one‑time scans or strongly repeating “hot” items.

What’s the time complexity of LRU cache operations?

Both get() and put() run in O(1) average time when implemented using a HashMap combined with a doubly linked list.

Can I implement LRU without a linked list?

Yes — Java’s LinkedHashMap with accessOrder=true internally manages ordering for you. You can also use arrays or other structures, but they usually sacrifice O(1) time complexity.

Does Redis implement true LRU?

By default, Redis uses an approximate LRU algorithm based on random sampling for performance reasons, not a perfectly precise doubly‑linked‑list LRU. It’s very close in practice and configurable.

How is LRU different from a TTL (Time‑To‑Live) cache?

TTL removes items after a fixed time period regardless of usage. LRU removes items based on relative usage recency compared to other items, regardless of absolute time. Many production systems combine both.

Is LRU used in hardware, or only in software?

Both. LRU (or pseudo‑LRU, a cheaper approximation) is commonly implemented directly in CPU cache hardware, not just in software systems.

21

Summary & Key Takeaways

Let’s step back and look at the big picture one more time. Every computer system, no matter how large or small, faces the same basic tension: fast storage is expensive and limited, while the amount of data that could be stored is essentially unlimited. LRU is one elegant, well‑tested answer to that tension. It doesn’t require predicting the future, doesn’t require complicated statistics, and doesn’t require expensive computation — it simply asks one simple question every time: “what have I not touched in the longest time?” and removes that. This simplicity is exactly why it has survived, largely unchanged in concept, from 1960s mainframe memory management all the way to the phone in your pocket and the servers powering today’s largest websites.

As you continue studying system design or building real applications, you’ll find that understanding LRU deeply — not just being able to recite the definition, but understanding why the HashMap and doubly linked list combination is necessary, why real systems use approximations at scale, and where LRU’s weaknesses show up — will serve you well, whether you’re debugging a slow production system, designing a new caching layer, or answering a technical interview question.

  • Takeaway 1LRU evicts the item that has gone unused for the longest time — it is built on the real‑world pattern that recently used things are likely to be needed again soon.
  • Takeaway 2The classic efficient implementation combines a HashMap (for instant lookup) with a doubly linked list (for instant reordering), giving O(1) get() and put().
  • Takeaway 3LRU powers systems at every layer: CPU caches, operating systems, databases, browsers, CDNs, and distributed caches like Redis.
  • Takeaway 4Plain LRU has a known weakness — vulnerability to pollution from one‑time scans — solved by variants like LRU‑K, 2Q, SLRU, and ARC.
  • Takeaway 5Production systems combine LRU with TTL expiration, sharding, replication, and monitoring to build reliable, scalable caching layers.
  • Takeaway 6Java offers two implementation paths: the quick LinkedHashMap approach, and the manual HashMap + doubly linked list approach expected in interviews and system design discussions.
  • Takeaway 7Never treat a cache as a source of truth — it is a performance optimisation. Always keep the durable store authoritative and let the cache be a fast, replaceable copy.

Leave a Reply

Your email address will not be published. Required fields are marked *