Skip to content

Benchmarks

Measured on an Apple M5 (10 cores), loopback, with redis-benchmark -n 100000 -c 50.

ScenarioSET QPSGET QPSp50 Latency
Baseline (no eviction)62,18965,2310.42ms
Pipelined -P 16350,877378,7871.06ms
LFU (16MB cap)57,33961,6900.42ms
AHE (16MB cap)59,55950,7870.42ms

Reading the numbers

  • Pipelining is the biggest lever — batching commands cuts per-request overhead and multiplies throughput.
  • QPS and latency only tell you how fast the engine serves requests. They say nothing about what an eviction algorithm is actually for: keeping the working set cached. The table below closes that gap.

Hit ratio by eviction policy

This is the metric a cache eviction algorithm is judged on, and the one the raw QPS numbers cannot show: under a realistic access pattern, how much of the working set stays cached. Measured end-to-end against a live server (the same method as the QPS runs above) with a working set of 100,000 distinct keys and a cache capped at 5,000 entries (~590 KiB) — i.e. the cache holds only 1/20th of the keyspace, so eviction is under constant pressure.

Policyzipf (stable skew)shift (rotating hot set)mixed (hot set + scan)scan (pure sequential)
allkeys-lru59.5%52.4%56.3%0.0%
allkeys-lfu59.4%51.1%58.0%0.0%
allkeys-ahe59.5%52.3%56.8%0.0%
allkeys-random57.1%52.4%54.5%0.0%

Higher is better. Each cell replays the same seeded workload against a fresh server pinned to that policy; the value is the server's own keyspace_hits / (keyspace_hits + keyspace_misses) over the whole run (cold start included). Reproduce with scripts/bench-hit-ratio.sh; the harness lives in examples/hit_ratio_bench.rs.

Methodology

  • Read-through client: a GET miss populates the key with a SET, exactly like a real cache fill. Eviction only engages once the cache is full, which is the regime that matters.
  • Realistic inter-request spacing (≈1 ms). FerrumKV's LRU and AHE are time-aware: recency is measured against a 600-second horizon and the AHE controller feeds on the observed hit ratio. A tight in-process loop finishes in milliseconds, flattening every recency signal and disabling the adaptive loop — so AHE would silently collapse to plain LFU and the comparison would be meaningless. Driving a live server with real spacing lets recency and the adaptive loop behave exactly as they do in production.
  • One fresh server per (policy, pattern) so counters never leak across workloads.
  • Patterns: zipf (stable Zipfian skew — the easy case), shift (the hot band rotates every epoch — a non-stationary workload), mixed (a small stable hot set plus a periodic full scan — OLTP-like), and scan (pure sequential — the honest "every policy is hopeless here" control). A fifth pattern, ttl, is also available (--patterns ttl) and mixes a durable hot set with a short-TTL ephemeral set to exercise TTL-aware eviction; in our measurements it converges with LRU/LFU at realistic cache sizes and does not reliably beat LFU at very tight caches, so it is opt-in rather than part of the headline matrix above.
  • Single representative run; figures vary ~±1 pp across runs because the engine's internal LFU/LRU sampling RNG is seeded from the wall clock.

What the numbers say

  • Stable skew (zipf): every policy converges to ~59%; AHE matches the leaders.
  • Rotating hot set (shift): LFU's sticky frequency counters collapse to 51.1% while LRU and AHE hold at ~52.3–52.4% — AHE tracks LRU and avoids LFU's worst case.
  • Mixed (mixed): LFU leads (58.0%) and LRU dips to 56.3%; AHE sits at 56.8% — near LRU, well clear of random's 54.5% floor.
  • Pure scan (scan): every policy is pinned at ~0%, confirming AHE does not magic away a hopeless access pattern.

The takeaway: AHE is the no-regret choice. On each workload it tracks the better of LRU and LFU, and it never suffers either policy's worst-case collapse (LFU on a shifting hot set, LRU under a scan-heavy mix). That adaptivity — not a fixed bias toward recency or frequency — is the point of the algorithm.

The full QPS methodology and raw output live in benches/redis-benchmark.md.