Intermediate10 min readlive prototype

2Q — Two Queue

A small FIFO probation queue protects an LRU main queue from being flushed by scans.

Overview

What this concept solves

2Q — Two Queue — is the simplest answer to 'how do I make LRU survive a scan?' Theodore Johnson and Dennis Shasha published it in 1994 as a buffer-pool replacement that consistently beats LRU on database workloads. The idea: new entries are on probation in a small FIFO queue. Only if they're re-accessed do they get promoted to the main LRU. Scans, which never re-access, never make it past probation.

MySQL InnoDB ships a 2Q variant as its default. So do several CDN edge caches. It's cheap, easy to implement, and gives you most of ARC's scan resistance with far less complexity.

Mechanics

How it works

Two queues, a single promotion rule

There are two cached lists plus a ghost list:

  • A1in — a small FIFO queue, typically ~25% of capacity. First-time entries land here.
  • Am — a large LRU queue, the remaining ~75%. The 'proven' working set lives here.
  • A1out (ghost) — keys recently evicted from A1in, no values. Used to detect 'almost popular' entries.

The algorithm on access:

  1. Hit in Am — move to head of Am (standard LRU touch).
  2. Hit in A1indon't promote yet. Leave it in place. (This is the version called 2Q-simplified; the original 2Q-Full promotes here.)
  3. Hit in A1out (ghost) — the key was kicked out of A1in but is asked for again, so it's worth keeping. Insert into Am at the head.
  4. Miss everywhere — insert into A1in at the head. If A1in is over its budget, push its tail into A1out (just the key) and drop the value.

Why this kills scans

A scan reads N unique keys, each exactly once. They all enter A1in as misses, and they all get pushed out of A1in (and into A1out) in FIFO order without ever being promoted to Am. Your hot Am entries — the things your real workload cares about — are never touched.

Interactive prototype

Run it. Break it. Tune it.

Sandboxed simulation embedded right in the page. No setup, no install.

About this simulation

Three lists. New keys land in A1in (small FIFO probation, top-left). When A1in overflows, the evicted key drops into A1out (ghost list, bottom) — keys only, no values. A future hit on a ghost promotes the key into Am (the real LRU cache, top-right). Watch the demo: A enters A1in, becomes a ghost, then on its next access gets promoted to Am.

Hands-on

Try these on your own

Open the prototype above, run each experiment, predict the answer, then verify.

try 01

Walk a key from A1in to ghost to Am

Click A. It lands in A1in. Click B, then C — A is pushed out of A1in and shows up dashed in A1out as a ghost. Now click A again. The status line says GHOST HIT — A is promoted directly into Am.

try 02

Watch a scan get absorbed

Click Reset. Click A twice to plant it in A1in. Now scan: D, E, F, B. Watch A1in cycle through them while Am stays empty — none of the scan keys are accessed twice in time, so none of them earn promotion.

try 03

An A1in re-hit doesn't promote

Click A, then immediately click A again. The status confirms 'HIT A1in (no reorder)'. 2Q-simplified only promotes via the ghost path — re-hits inside A1in are noted but don't move the key.

In practice

When to use it — and what you give up

When to reach for it

  • Buffer pools for storage engines — MySQL InnoDB ships a 2Q variant by default.
  • Workloads that intermittently scan — analytics queries against an OLTP database, occasional batch jobs against a live cache.
  • You want scan resistance without ARC's complexity or patent.

Real-world example

MySQL InnoDB splits the buffer pool into a 'young' sublist (≈63%) and an 'old' sublist (≈37%), with promotion gated by a time-on-old threshold. The shape is 2Q — different names, same idea.

Pros

  • Strong scan resistance — the dominant reason it's chosen in database buffer pools.
  • Conceptually simple — two lists and a small ghost list, with a single promotion rule.
  • Empirically competitive with ARC on most workloads, at much lower implementation cost.
  • Patent-free.

Cons

  • Has tuning parameters: the size ratio between A1in, Am, and A1out. Not adaptive.
  • More moving parts than plain LRU — three lists vs one.
  • If the workload is purely recency-driven (no scans), plain LRU does about as well with less code.

Reference

Code & further reading

A minimal reference implementation and pointers worth bookmarking.

two-q.ts
// 2Q simplified: A1in (FIFO), Am (LRU), A1out (ghost set).
class TwoQ<K, V> {
  private a1in = new Map<K, V>();   // FIFO probation
  private am = new Map<K, V>();      // LRU main
  private a1out = new Set<K>();      // ghost
  constructor(
    private kIn: number,    // capacity of A1in
    private kOut: number,   // capacity of A1out (ghost)
    private kMain: number,  // capacity of Am
  ) {}

  get(key: K): V | undefined {
    if (this.am.has(key)) {
      const v = this.am.get(key)!;
      this.am.delete(key); this.am.set(key, v); // touch (LRU)
      return v;
    }
    if (this.a1in.has(key)) {
      return this.a1in.get(key)!; // do NOT promote on first re-hit
    }
    return undefined;
  }

  set(key: K, value: V): void {
    if (this.am.has(key) || this.a1in.has(key)) return; // already cached
    if (this.a1out.has(key)) {
      // Ghost hit: promote straight to Am
      this.a1out.delete(key);
      this.evictMainIfNeeded();
      this.am.set(key, value);
      return;
    }
    // Brand-new key -> A1in
    if (this.a1in.size >= this.kIn) {
      const oldest = this.a1in.keys().next().value!;
      this.a1in.delete(oldest);
      this.a1out.add(oldest);
      if (this.a1out.size > this.kOut) {
        const ghostOldest = this.a1out.values().next().value!;
        this.a1out.delete(ghostOldest);
      }
    }
    this.a1in.set(key, value);
  }

  private evictMainIfNeeded(): void {
    if (this.am.size >= this.kMain) {
      const oldest = this.am.keys().next().value!;
      this.am.delete(oldest);
    }
  }
}

References & further reading

3 sources

Knowledge check

Did the prototype land?

Quick questions, answers revealed on submit. No scoring saved.

question 01 / 03

Why does 2Q resist scans better than LRU?

question 02 / 03

What is the role of the A1out ghost list?

question 03 / 03

Which production system most prominently uses a 2Q-style policy?

0/3 answered

Was this concept helpful?

Tell us what worked, or what to improve. We read every note.