mithril

Benchmarks

Realistic deployment matrix (cocoon-test, 2026-08-30)

Host: 384-core Linux box shared with other work. Backing cluster: 32 nodes (16 masters + 16 replicas, redis 8.10) pinned to 32 cores. Every proxy runs 8 worker threads on its own 8 cores; the load generator runs on separate cores (memtier_benchmark 8 threads, 20-second windows; redis-benchmark 8 threads, 12M requests). Arms are interleaved in one run with the order reversed on the second pass; the ranges below are the two passes. 64-byte values unless stated, 100k keys, random keys, SET:GET 1:1 unless stated.

Columns: mithril (backend-sharding no, the default until v0.1.6), shard (backend-sharding yes), cache (reply-cache yes), shard+cache (both), mt-proxy (C++), predixy (C++). Commit 9336a89 lineage.

Throughput (ops/s)

cell mithril shard cache shard+cache mt-proxy predixy
P1, 200 conns 429k 468-470k 597-607k 487-491k 470k 405-407k
P1, 400 conns 485-487k 554-555k 614-615k 548-564k 569-576k 468-475k
P1, 400 conns, GET only 484k 555-560k 621-623k 603-608k 577-578k 469-471k
P1, 4 KiB values 387-393k 406k 530-538k 445-446k 415-416k 354-357k
P4, 512 B values 950-959k 1.07M 1.20-1.22M 1.37M 958-961k 970-977k
P16 2.72-2.78M 2.70M 3.04-3.05M 3.12-3.15M 2.32M 3.02-3.03M
P16, 4 KiB values 944-957k 933-939k 995k-1.01M 998k-1.00M 911-917k 932-941k
redis-benchmark GET, P1, 400 conns 414k 494-505k 489-494k 489k 480-484k 428k

Tail latency (p99 / p999, ms)

cell mithril cache shard+cache mt-proxy predixy
P1, 200 conns 0.60 / 0.67 0.49 / 0.58-0.61 0.73 / 0.90-0.92 0.70 / 0.80 0.66 / 0.74-0.76
P1, 400 conns 1.08 / 1.40-1.58 0.95 / 1.23-1.32 1.30-1.32 / 1.70-1.88 1.13-1.14 / 1.40-1.47 1.11-1.13 / 1.49-1.72
P1, 4 KiB values 0.68-0.69 / 0.82-0.86 0.61-0.63 / 0.82-0.92 0.91 / 1.15-1.16 0.80 / 0.93-0.94 0.75-0.76 / 0.86-1.02
P4, 512 B values 1.21-1.23 / 1.30-1.38 0.94-0.95 / 1.06-1.07 1.02-1.03 / 1.26-1.30 1.30 / 1.51-1.54 1.07 / 1.14-1.30
P16 1.54-1.58 / 1.62-1.75 1.47-1.48 / 1.60-1.66 1.75-1.78 / 2.08-2.37 2.21 / 2.51-2.53 1.45 / 1.68-1.75
P16, 4 KiB values 4.38-4.45 / 5.15-6.91 4.29-4.35 / 6.30-7.07 4.58-4.74 / 8.32-8.64 5.34-5.54 / 6.21-7.26 4.19 / 5.38-5.79

Reading it: with the reply cache on, mithril holds the throughput lead in every cell (the redis-benchmark ceiling goes to its sharding mode, with cache and shard+cache inside 2%) and the best p99 in six of eight; predixy keeps a 2-3% p99 edge at pipeline 16 while losing on p999. The cache does not need a read-only workload: these cells are 1:1 SET/GET, and the server-tracked invalidation keeps the cache coherent — the same session always reads its own writes, other sessions see a write within the invalidation push latency.

Without the cache, mithril’s default mode is the tail-latency choice in the pipelined cells and mt-proxy’s single-connection-per-node design wins the unpipelined saturation cells — which backend-sharding recovers.

backend-sharding auto (same rig, commit e6e1592 lineage)

The table was measured with the first auto, whose worker rule moved sessions on busyness alone; the paragraph after it has the current mechanism. Two rounds each, redis-benchmark unless stated:

cell default shard auto
P16, 1000 conns GET 5.58M / p50 2.86 8.95M / 0.94 8.85-8.91M / 0.93
P16, 1000 conns SET 5.54M / 2.89 9.02M / 0.93 9.13-9.20M / 0.91
P16, 2000 conns GET 6.70M / 4.52 9.67M / 1.86 10.4M / 1.8
P64, 1000 conns GET 10.66M / 5.80 12.62M / 2.89 10.8-14.4M / 1.9-3.0
P1, 2000 conns GET 2.20M / 0.89 2.63M / 0.71 2.66M / 0.70
P16, 256 conns GET 4.78M / 0.84 5.49M / 0.33 5.56-5.66M / 0.31
memtier P16, 200 conns (sliding) 4.36M / 0.76 3.59M / 0.92 3.74M / 0.86
memtier P1, 400 conns 445k 443k 443k

That auto landed on the better of the two paths in every cell but one: a sliding-pipeline client at moderate concurrency on a partly idle proxy ran 14% slower than no, because its worker rule moved sessions on busyness alone. Since v0.1.7 auto runs one experiment for the whole proxy and keeps the shared pipes only where the proxy’s command rate measures higher (see architecture.md); on the same rig, with 90-second GET and 60-second memtier cells over three rotated rounds: P16 1000 conns GET no 5.54M, auto 8.98M, yes 9.01M; the sliding memtier cell no 4.31M, auto 4.23M, yes 3.58M; P1 2000 conns and the cache cells at parity with the best fixed mode. The 2% in the sliding cell is the experiment itself (two two-second windows in a minute, then one per minute doubling to eight), which is why auto is now the default; set no only when a sliding pipeline is the whole workload and that 2% matters more than the 60% the other shapes gain.

Re-measured at v0.1.6 on the same rig (four rotated rounds, default / shard / auto): P16 1000 conns GET 5.55M / 8.87M / 8.76M and SET 5.54M / 8.93M / 9.05M, P1 2000 conns GET 2.18M / 2.60M / 2.61M, the sliding memtier cell 4.39M / 3.56M / 3.70M — the picture above stands. Every batch since v0.1.5 was A/B-measured against its base on the default-mode P16 1000-conns cell and sits within the A/A noise floor, so the release adds no per-request cost.

Bare-metal reference (2026-08, 4-worker proxies, 6-node cluster)

Method: memtier_benchmark on a 16-core bare-metal Linux host, cpuset-pinned (redis on 6 cores, the proxy under test on 4, memtier on 6), interleaved arms with the order swapped per round, 6 samples per arm, 64-byte values, 1:1 SET/GET over 100k keys. Backing cluster: redis 6.2, 3 masters + 3 replicas.

target pipeline=1 ops/s pipeline=16 ops/s
direct to cluster (no proxy) 763k 4.09M
mithril 944k 4.62M
mt-proxy (C++) 961k 3.28M
predixy 865k 4.35M

At pipeline 16 mithril is the fastest of everything measured — including the direct arm, because request batching reduces per-op syscall load on the single-threaded redis processes. At pipeline 1 mithril trails mt-proxy by ~1.7%: mt-proxy handles a request inline in a single epoll loop, while mithril pays reader-to-writer task handoffs that pipelining amortizes.

Numbers from Docker-for-Mac invert the ranking and must not be used for comparisons.