moon Benchmark Report¶
Release marker: the numbers below are the benchmark record backing v0.6.0
(README headline claims: peak GET/SET §2, p=1 busy-poll win §2.10, durability
write-path §7.3, multi-shard convoy fix §6.6, vector time-to-green vs Qdrant §10.9,
graph vs FalkorDB §11, engine-offload RSS reduction in docs/guides/tuning.md).
Last Updated: 2026-07-08 (new §7.3: durability write-path campaign (PRs #238–#242) — closes the AOF-on deficits vs Redis. appendfsync always P16 SET 0.12×→0.91× (per-command fsync → per-batch group commit + one coalesced write), everysec P1 SET 0.80×→0.99× parity (park-free writer poll kills a 150k/run futex-wake storm on the shard thread), everysec P16 SET 1.32× WIN, and pub/sub fan-out delivery 438 msg/s (near-total drops) → 5.09M msg/s, zero drops, ~1.04× Redis. All GCE c3-standard-8, Redis 7.0.15, 3 alternated reps, provenance-probed. Prior 2026-07-04 §6.6: multi-shard multi-connection WIN — reply-convoy fix + slot-unified replies take s4 c8 P1 from 0.44–0.64× to 1.57–2.0× Redis and c64 to 2.5×, both arches; supersedes §6.5's c≥8 rows. Prior 2026-07-03: §2.10: p=1 single-op WIN on both arches via --io-busy-poll-us poll-mode park — ARM c4a 1.19–1.21×, x86 c3 1.65–1.66× vs Redis, n=3 instances/arch; supersedes the "loses p=1" rows in §2.7–2.9 when busy-poll is on. §6.5: shards × busy-poll sweep — busy-poll recovers +22–42% of the cross-shard hop but shards>1 still loses non-pipelined; --shards 1 --io-busy-poll-us 40 is the p=1 config. Prior: 2026-06-16 4-feature concurrent-vs-competitor pass — §10.5 vector vs RediSearch, §11.4 graph vs FalkorDB, §12 Full-Text Search; §2.8 = v2-1/PR #189 db61973 K=1024 re-measurement; v0.1.6 in §2.1–2.6; §2.7 on perf/shard-dispatch-hot-path)
Platforms: Linux (GCloud x86_64 + ARM64), macOS (Apple M4 Pro)
Redis: 8.6.1 in §2.1–2.6; 7.0.15 in §2.7
moon: v0.1.6 in §2.1–2.6; perf/shard-dispatch-hot-path HEAD (commit 6582fa9) in §2.7. Monoio runtime (io_uring on Linux, kqueue on macOS), fat LTO, codegen-units=1, target-cpu=native
Methodology: Co-located benchmarks using redis-benchmark. Fresh server instance per data point for memory tests. All ratios from same-run comparisons to control for VM variance.
IMPORTANT — read §2.7.1 before comparing SET numbers across this report. Two redis-benchmark invocation styles are in play:
- Loose (redis-benchmark -t SET -P 64, no -r): every write hits the single key __rand_key__. Cache-hot, no dict growth, no key distribution pressure. Matches §2.1/§2.2 historical methodology.
- Strict (redis-benchmark -t SET -r 1000000 -P 64): writes distribute uniformly over 1M keys. Exercises actual dict growth, probe-path collisions, cache pressure. Matches production workloads.
The SET absolute number can differ 3-4× between methodologies. Only strict-vs-strict or loose-vs-loose comparisons are meaningful.
Table of Contents¶
- Executive Summary
- Linux GCloud Benchmarks
- Memory Efficiency
- Throughput
- CPU Efficiency
- Multi-Shard Scaling
- Persistence (AOF) Performance
- Production Workload Patterns
- Latency
- Vector Search
- Graph Engine
- Full-Text Search
- Data Correctness
- Architecture Notes
- How to Reproduce
1. Executive Summary¶
| Metric | moon vs Redis | Conditions |
|---|---|---|
| p=1 single-op GET/SET (x86) | 1.65-1.66x Redis | --io-busy-poll-us 40, GCE c3 dedicated, n=3, §2.10 |
| p=1 single-op GET/SET (ARM) | 1.19-1.21x Redis | --io-busy-poll-us 40, GCE c4a dedicated, n=3, §2.10 |
| Peak GET (Linux x86_64) | 5.11M ops/s (1.72x) | GCloud c3-standard-8, P=64 |
| Peak GET (Linux ARM64) | 3.47M ops/s (2.20x) | GCloud t2a-standard-8, P=64 |
| Peak GET (macOS) | 7.94M ops/s (2.59x) | OrbStack, Apple M4 Pro, P=64 |
| Production defaults GET | 1.93x Redis | appendonly=yes, disk-offload, P=64 |
| Memory (1KB+ values) | 27-35% less | 1-shard, per-key RSS |
| Memory (256B values) | Tied | 1-shard, per-key RSS |
| Baseline RSS (empty) | Identical (7.0 MB) | 1-shard |
| CPU efficiency at P=64 | 45x better | 1.9% vs 43.9% CPU for similar RPS |
| With AOF persistence | 2.75x Redis | SET, P=64, per-shard WAL |
| AOF everysec SET P16 | 1.32x Redis | group commit + coalesced write, §7.3 |
| AOF everysec SET P1 | 0.99x (parity) | park-free writer poll, §7.3 (was 0.80x) |
| AOF always SET P16 | 0.91x Redis | per-batch group commit, §7.3 (was 0.12x) |
| Pub/sub fan-out delivery | 5.09M msg/s, 1.04x Redis, 0 drops | 8 subs, coalesced writes, §7.3 (was 438 msg/s) |
| Multi-shard (8s P=16) | 1.84-1.99x Redis | GET / SET |
| p50 latency (8-shard) | 8-10x lower | 0.031ms vs 0.26ms |
| Data correctness | 2613+ tests pass | All types, 1/4/12 shards |
| Vector insert (384d) | 6–20× RediSearch | GCloud, 8-thread concurrent, §10.5 |
| Vector search (384d) | RediSearch ~16× QPS; recall 0.86 vs 0.96 | concurrent vs RediSearch, §10.5 |
| Full-text search (100K) | counts exact vs RediSearch; index ~0.85×, low-DF wins; multi-term QPS trails | vs RediSearch, §12.3 (v3-1 verified) |
| Graph build (native API) | 21–26× FalkorDB | §11.5 (native 1-hop ≈1.2× FalkorDB Cypher) |
| Graph Cypher point-query | correct + ~28–30× vs old full-scan; ~4× behind Falkor's indexed scan | §11.5 (v3-2 verified, match_rows=Falkor's) |
2. Linux GCloud Benchmarks¶
Date: 2026-04-15 Instances: x86_64 (c3-standard-8, Sapphire Rapids 8481C) / ARM64 (t2a-standard-8, Neoverse-N1), 8 vCPU, 32GB RAM, Ubuntu 24.04, kernel 6.8
2.1 Raw Throughput (no persistence)¶
Moon started with --appendonly no --disk-offload disable.
| Metric | x86_64 | ARM64 | Redis (x86_64) | Redis (ARM64) | Ratio (x86) | Ratio (ARM) |
|---|---|---|---|---|---|---|
| GET p=64 | 5.11M | 3.47M | 2.98M | 1.58M | 1.72x | 2.20x |
| SET p=64 | 3.50M | 2.42M | 1.82M | 1.15M | 1.92x | 2.10x |
| GET p=32 | 2.73M | — | 2.07M | — | 1.32x | — |
2.2 Production Defaults (appendonly=yes, disk-offload=enable, WAL v3, PageCache)¶
This is moon's out-of-the-box configuration. Reads are unaffected by persistence — PageCache mmap actually improves read locality.
| Metric | x86_64 | ARM64 | Redis (x86_64) | Redis (ARM64) | Ratio (x86) | Ratio (ARM) |
|---|---|---|---|---|---|---|
| GET p=64 | 4.76M | 3.45M | 2.46M | 1.61M | 1.93x | 2.14x |
| SET p=1 | 147K | — | 136K | — | 1.08x | — |
| SET p=64 | 1.05M | — | 1.83M | — | 0.57x | — |
Key insight: GET throughput is identical across all persistence modes — reads are free. SET at high pipeline (p=64) pays ~50% WAL overhead due to per-shard fsync, but SET at p=1 still beats Redis even with full WAL.
2.3 Max Durability (appendfsync always)¶
| Metric | x86_64 | Redis (x86_64) | Ratio |
|---|---|---|---|
| GET p=64 | 4.85M | 2.45M | 1.98x |
2.4 Platform Comparison¶
| Platform | Moon GET p=64 | Redis GET p=64 | Ratio |
|---|---|---|---|
| GCloud c3-standard-8 (x86_64) | 5.11M | 2.98M | 1.72x |
| GCloud t2a-standard-8 (ARM64) | 3.47M | 1.58M | 2.20x |
| OrbStack (Apple M4 Pro, aarch64) | 7.94M | 3.07M | 2.59x |
x86_64 is ~1.4x ARM64 (Sapphire Rapids vs Neoverse-N1). OrbStack gives best absolute numbers due to no noisy-neighbor effect.
2.5 GCloud Variance Warning¶
GCloud VM results vary 10-15% between runs due to noisy-neighbor CPU sharing. Both Redis and Moon are equally affected. Always compare Moon/Redis ratios from the same run, not absolute RPS across different runs.
| Run | Zone | Moon GET p=64 | Redis GET p=64 | Ratio |
|---|---|---|---|---|
| Apr 6 | us-central1 | 5.52M | 2.36M | 2.34x |
| Apr 15 #1 | us-central1-a | 4.59M | 2.46M | 1.87x |
| Apr 15 #2 | us-east1-b | 5.05M | 2.84M | 1.78x |
| Apr 15 #3 | us-central1-a | 5.11M | 2.98M | 1.72x |
The Apr 6 ratio (2.34x) was inflated by unusually slow Redis (2.36M). True GCloud c3 ratio is ~1.75x.
2.6 Memory Stability¶
RSS flat at 12.5MB under 100s sustained load (3 burst cycles of 1M requests each). No memory leak from tick-based event loop.
2.7 2026-04-22 Re-measurement (perf/shard-dispatch-hot-path HEAD)¶
Branch: perf/shard-dispatch-hot-path at commit 6582fa9. Three new commits landed on top of v0.1.6-era baseline:
| commit | fix | effect |
|---|---|---|
e2addc8 |
pre-size DashTable + fuse Database::set probe |
eliminates 9.89% split_segment CPU, halves hit-path probes |
e00769e |
length-gate + #[inline] try_handle_* |
cuts ~5pp of per-command dispatch overhead |
6582fa9 |
batch-level eviction gate skips per-write runtime_config lock |
handler-self closure -3.2pp when maxmemory=0 and disk-offload disabled |
Instances: fresh provisions, same class as §2.1 (c3-standard-8 x86_64 us-central1-a, t2a-standard-8 ARM64 us-central1-f). Redis: Ubuntu 24.04 package 7.0.15 (not the 8.6.1 used in §2.1-2.6). CPU pinning: server CPU 1, bench CPUs 2-5 via taskset.
2.7.1 Methodology — strict vs loose¶
The v0.1.6 §2.1 table reported SET p=64 = 3.50M x86 / 2.42M ARM. Those numbers were measured with the default redis-benchmark -P 64 -t SET (no -r flag), which writes every request to the single key __rand_key__. That degenerates the workload: same segment every time, no dict growth, no key-distribution pressure, cache-hot throughout. It is what Redis's own benchmark folklore uses, but it does not reflect any real workload.
The strict benchmark adds -r 1000000, spreading writes uniformly over 1M distinct keys. This exercises:
- DashTable segment splits during table growth
- h2 fingerprint collisions across the full keyspace
- Cache pressure on the keys array
- CompactKey heap allocations for keys beyond the 22-byte inline threshold
Strict numbers are always lower. Moon gains more from loose methodology than Redis does (Moon's probe path amortizes better when the segment is cache-hot), so strict comparisons are the more honest "Moon vs Redis" signal.
Both methodologies shown below. Pick the one matching your deployment — interactive cache workloads with uniform hot keys look like loose; real keyspaces look like strict.
2.7.2 Strict methodology (-r 1000000, distributed keyspace)¶
| op | p | x86 Moon fair | x86 Moon default | x86 Redis | Ratio (fair) | ARM Moon fair | ARM Moon default | ARM Redis | Ratio (fair) |
|---|---|---|---|---|---|---|---|---|---|
| GET | 64 | 4.50M | 4.55M | 2.86M | 1.58× | 3.03M | 3.11M | 2.02M | 1.50× |
| GET | 16 | 1.50M | 1.52M | 1.76M | 0.85× | 1.06M | 1.07M | 1.27M | 0.83× |
| GET | 1 | 108K | 106K | 132K | 0.82× | 76K | 79K | 100K | 0.76× |
| SET | 64 | 1.29M | 0.82M | 1.08M | 1.19× | 752K | 552K | 871K | 0.86× |
| SET | 16 | 962K | 668K | 859K | 1.12× | 564K | 437K | 681K | 0.83× |
| SET | 1 | 107K | 108K | 138K | 0.77× | 84K | 97K | 100K | 0.84× |
"Moon fair" = --appendonly no --disk-offload disable --initial-keyspace-hint 1000000. "Moon default" = same minus --disk-offload disable (disk-offload ON). See §2.7.4 for the tax.
2.7.3 Loose methodology (no -r, matches §2.1 v0.1.6 shape)¶
| op | p | x86 Moon fair | x86 Moon default | x86 Redis | Ratio (fair) | ARM Moon fair | ARM Moon default | ARM Redis | Ratio (fair) |
|---|---|---|---|---|---|---|---|---|---|
| GET | 64 | 5.15M | 5.10M | 2.84M | 1.82× | 3.50M | 3.65M | 2.02M | 1.73× |
| GET | 16 | 1.59M | 1.60M | 1.76M | 0.90× | 1.19M | 1.17M | 1.29M | 0.92× |
| GET | 1 | 109K | 108K | 135K | 0.81× | 77K | 78K | 101K | 0.76× |
| SET | 64 | 4.46M | 1.69M | 2.02M | 2.21× | 3.42M | 1.29M | 1.45M | 2.36× |
| SET | 16 | 1.57M | 1.23M | 1.41M | 1.12× | 1.17M | 876K | 1.01M | 1.16× |
| SET | 1 | 108K | 107K | 136K | 0.79× | 79K | 87K | 100K | 0.79× |
2.7.4 Disk-offload tax (5-run SET p=64 means, CV 2-8%)¶
--disk-offload defaults to enable in Moon's CLI. Even when the workload never exceeds RAM, every write pays for try_evict_if_needed_async_spill, spill_file_id.get/set, and the per-shard spill thread's cache-coherency traffic. Redis has no equivalent — disable this flag for Moon-vs-Redis comparisons.
| arch | methodology | Moon fair | Moon default | Redis | Moon fair/Redis | Disk-offload tax |
|---|---|---|---|---|---|---|
| x86 | strict | 1.33M | 812K | 1.12M | 1.19× | -39% |
| x86 | loose | 4.46M | 1.69M | 1.97M | 2.26× | -62% |
| ARM | strict | 846K | 617K | 849K | 1.00× | -27% |
| ARM | loose | 3.44M | 1.28M | 1.44M | 2.39× | -63% |
The disk-offload tax is larger on the loose (cache-hot) workload because when DashTable work is cheap, the spill-thread bookkeeping represents a larger fraction of total cost.
2.7.5 Delta vs v0.1.6 §2.1 (same arch, same class, same loose methodology)¶
| arch | metric | v0.1.6 §2.1 | Today §2.7.3 | Δ |
|---|---|---|---|---|
| x86 | GET p=64 | 5.11M | 5.15M | +1% (flat) |
| x86 | SET p=64 | 3.50M | 4.46M | +27% |
| ARM | GET p=64 | 3.47M | 3.50M | +1% (flat) |
| ARM | SET p=64 | 2.42M | 3.42M | +41% |
The three session commits (A+B, E, D) land a real +27% x86 / +41% ARM SET p=64 improvement over the v0.1.6 tag, with GET p=64 holding flat. Redis 7.0.15 (§2.7) vs Redis 8.6.1 (§2.1) is different — the ratio change is Moon moving up, not Redis moving down (Redis x86 GET p=64 went from 2.98M §2.1 to 2.84M §2.7 — essentially flat).
2.7.6 Caveats¶
- GCloud VM hurts p=1 / p=16 workloads. At low pipeline depth, TCP RTT dominates per-op cost. GCloud VM network stack is slower than OrbStack's bridged interface. On OrbStack ARM the same branch wins all p=1/p=16 workloads; on GCloud x86/ARM it loses them. This is a VM-class artifact, not a Moon regression.
- ARM strict SET p=64 ratio is 0.86× (Moon loses on Neoverse-N1). The Neoverse-N1 has lower per-core IPC than Sapphire Rapids 8481C; Moon's per-command tax (Frame ref-counting, AffinityTracker sample, metric record) eats more of the budget on ARM.
- Variance. Strict SET p=64 5-run CV is 2-4% (low). The loose ARM column has one outlier run at 1.12M vs 750K-800K elsewhere — kept in the mean, produces inflated σ. Re-running would give a cleaner number, but the directional finding (Moon wins loose, loses strict on ARM) is robust.
2.8 2026-06-15 Re-measurement (v2-1-throughput-polish / PR #189, K=1024 yield)¶
Branch: feat/ft-yield-costfree-monoio at commit db61973 (current main + PR #189 — the cost-free monoio FT.SEARCH yield with the cross-arch K=1024 brute-force knee). PR #189 touches only the FT.SEARCH brute-force yield path, so KV/multi-shard/graph throughput is expected unchanged from §2.1/§2.7 — this run confirms that (KV stays within GCloud's 10-15% VM variance of the prior baseline; no regression).
Instances: fresh on-demand provisions, same class as §2.1 — x86_64 (c3-standard-8, Intel Xeon Platinum 8481C @ 2.70GHz, 8 vCPU, 31 GiB) us-central1-a, ARM64 (t2a-standard-8, Neoverse-N1, 8 vCPU, 31 GiB) us-central1-a, Ubuntu 24.04. Redis: Ubuntu 24.04 package 7.0.15 (same as §2.7, not the 8.6.1 of §2.1-2.6). Build: RUSTFLAGS="-C target-cpu=native", fat LTO, cgu=1 (x86 3m51s, ARM 4m36s). No CPU pinning this run (unlike §2.7's taskset) — absolute RPS is therefore lower than §2.7's pinned figures; same-run Moon/Redis ratios are the signal. redis-benchmark -c 50 -n 400000, best-of-3 per cell. "Moon fair" = --appendonly no --disk-offload disable --initial-keyspace-hint 1000000.
2.8.1 Loose methodology (no -r, single hot key — matches §2.1 shape)¶
| op | p | x86 Moon | x86 Redis | Ratio | ARM Moon | ARM Redis | Ratio |
|---|---|---|---|---|---|---|---|
| GET | 64 | 4.65M | 2.44M | 1.91× | 3.74M | 1.65M | 2.26× |
| GET | 16 | 1.53M | 1.59M | 0.97× | 1.03M | 1.07M | 0.97× |
| GET | 1 | 109K | 136K | 0.80× | 74K | 99K | 0.74× |
| SET | 64 | 3.08M | 1.82M | 1.69× | 2.63M | 1.27M | 2.07× |
| SET | 16 | 1.89M | 1.30M | 1.45× | 1.42M | 893K | 1.59× |
| SET | 1 | 108K | 135K | 0.80× | 76K | 104K | 0.73× |
x86 loose GET p=64 4.65M is within ~9% of §2.1's 5.11M / §2.7.3's 5.15M (no-pinning VM variance); loose SET p=64 3.08M tracks §2.1's 3.50M (no-pinning) rather than §2.7.3's 4.46M (pinned). No KV regression from PR #189.
2.8.2 Strict methodology (-r 1000000, distributed keyspace)¶
| op | p | x86 Moon | x86 Redis | Ratio | ARM Moon | ARM Redis | Ratio |
|---|---|---|---|---|---|---|---|
| GET† | 64 | 1.20M | 969K | 1.24× | 980K | 798K | 1.23× |
| GET† | 16 | 1.01M | 849K | 1.19× | 745K | 676K | 1.10× |
| GET† | 1 | 108K | 134K | 0.81× | 70K | 99K | 0.71× |
| SET | 64 | 930K | 771K | 1.21× | 835K | 657K | 1.27× |
| SET | 16 | 766K | 653K | 1.17× | 611K | 535K | 1.14× |
| SET | 1 | 107K | 144K | 0.75× | 73K | 107K | 0.68× |
† Strict GET here is a miss workload — this harness runs GET before SET in each pipeline group, so the 1M-key GETs hit an empty table. That makes strict GET p=64 (1.20M x86) lower than §2.7.2's warm-hit strict GET (4.50M, keyspace pre-populated). The strict SET rows are the honest distributed-write signal: Moon wins strict SET on both arches this run (x86 1.21×, ARM 1.27×) — note ARM strict SET p=64 flipped positive vs §2.7.2's 0.86× loss, well within the 2-4% strict CV plus the no-pinning delta.
2.8.3 Production defaults (Moon appendonly=yes everysec + disk-offload ON; Redis AOF everysec)¶
| op | p | x86 Moon | x86 Redis | Ratio | ARM Moon | ARM Redis | Ratio |
|---|---|---|---|---|---|---|---|
| GET | 64 | 4.71M | 2.34M | 2.01× | 3.57M | 1.63M | 2.19× |
| GET | 1 | 107K | 136K | 0.79× | 68K | 102K | 0.67× |
| SET | 64 | 605K | 1.66M | 0.36× | 591K | 1.23M | 0.48× |
| SET | 1 | 133K | 136K | 0.97× | 94K | 105K | 0.90× |
Reads are free under persistence (GET p=64 2.0-2.2×, matching §2.2). Single-shard SET p=64 with everysec fsync is 0.36-0.48× Redis — the documented per-shard WAL cost at high pipeline depth on one shard (CLAUDE.md: "WAL sync kills write throughput"). The per-shard-WAL advantage over Redis's single AOF (§7) materializes with more shards, not at shards=1.
2.8.4 Multi-shard scaling (Moon-only, fair loose) — confirms single-shard-is-best¶
| arch | shards | GET p=16 | SET p=16 | GET p=64 | SET p=64 |
|---|---|---|---|---|---|
| x86 | 1 | 1.59M | 1.90M | 4.71M | 3.08M |
| x86 | 4 | 1.58M | 1.86M | 4.76M | 3.01M |
| x86 | 8 | 1.56M | 1.83M | 4.76M | 2.99M |
| ARM | 1 | 1.01M | 1.40M | 3.60M | 2.52M |
| ARM | 4 | 1.00M | 1.38M | 3.45M | 2.42M |
| ARM | 8 | 969K | 1.33M | 3.42M | 2.41M |
Scaling 1→8 shards is flat-to-slightly-negative for uniform single-key GET/SET at c=50: x86 GET p=64 holds at 4.7M (loopback-network ceiling), GET p=16 dips −2% (cross-shard SPSC dispatch cost); ARM dips −4 to −5%. This confirms the CLAUDE.md gotcha — most keys route cross-shard, so SPSC dispatch overhead dominates the local DashTable lookup; use --shards 1 unless exploiting pipeline/AOF parallelism or hash-tag co-location. It refines §4.4's optimistic 1.46×-at-8-shards figure, which reflected a different (non-uniform / higher-concurrency) workload, not uniform-key GCloud routing.
2.8.5 Graph + vector confirmation¶
Graph (bench-graph-compare.sh --moon-only --nodes 2000): x86 node 290/s · edge 301/s · Cypher 297/s; ARM 213/s · 231/s · 214/s — within ~3% of §11.1/§11.2 (no regression). Vector (50K×384d COSINE, single-connection pipelined harness): insert 19.2K vec/s x86 / 22.0K ARM; the full lifecycle insert→brute-force→FT.COMPACT→HNSW search ran clean and HNSW was ~10× faster per query than brute-force (240 vs 25 QPS x86 single-conn latency-bound), confirming the K=1024 yield change does not break search. These latency-bound single-conn figures are not comparable to §10's concurrent-throughput 12.7K QPS and do not supersede it.
2.8.6 Caveats¶
- No CPU pinning (unlike §2.7) → absolute RPS sits below §2.7's pinned numbers; rely on same-run ratios.
- Strict GET = miss workload (GET-before-SET ordering); strict SET is the honest distributed-write number (see †).
- Per-key memory not recorded — the harness failed to capture Redis RSS (empty), so no Moon-vs-Redis per-key comparison was possible this run; §3 stands unchanged. Moon-only RSS for ~63K keys (coupon-collector over
-r 100000) was sane (x86: 32B 24.0 MB, 256B 38.3 MB, 1KB 90.2 MB total incl. ~11 MB base). - Vector harness is single-connection latency-bound (see §2.8.5).
2.9 2026-06-17 KV confirmation (v3-1/v3-2 verified build, 8238515)¶
The 4-feature verified cross-arch run (KV slice; full report docs/reviews/2026-06-17/4FEATURE-VERIFIED.md)
re-confirmed KV is unaffected by v3-1-fts-hardening / v3-2-graph-correctness (text + graph paths only).
Moon-fair vs Redis (ratio = Moon/Redis), 8-vCPU c3/t2a, best-of-3:
| arch | mode | GET P64 | SET P64 | GET P16 | SET P16 | GET P1 | SET P1 |
|---|---|---|---|---|---|---|---|
| x86 | loose | 1.87× (4.44M) | 1.69× (2.99M) | 0.96× | 1.43× | 0.81× | 0.82× |
| x86 | strict | 1.15× | 1.14× | 1.11× | 1.12× | 0.83× | 0.77× |
| ARM | loose | 1.85× (2.99M) | 2.05× (2.50M) | 0.95× | 1.34× | 0.82× | 0.82× |
| ARM | strict | 1.27× | 1.32× | 1.12× | 1.19× | 0.83× | 0.80× |
Within VM variance of §2.8 (x86 loose P64 1.87–1.90×). Moon wins at pipeline depth, loses p=1 (TCP-RTT bound). No regression.
2.10 2026-07-03 p=1 single-op WIN — --io-busy-poll-us poll-mode park (branch perf/v3-3-p1-hotpath, 6178a71)¶
The historical "loses p=1" rows (§2.7–2.9 P1 columns, 0.77–0.83×) are superseded when busy-poll is
enabled. Root cause of the p=1 deficit was attributed by perf tracepoints on GCE: both engines
sleep+wake the server thread on every non-pipelined op; Redis's 3-syscall epoll loop rode that path
slightly cheaper than Moon's drivers (io_uring's 2 enters cost more than 3 light syscalls on GCE;
Moon-epoll paid 4 syscalls/op from a speculative-read EAGAIN). --io-busy-poll-us <µs> removes the
sleep entirely: the shard thread busy-loops zero-timeout readiness polls for the budget before
blocking (vendored-monoio LegacyDriver patch; flag forces the epoll driver). Redis keeps paying the
wake on every op — the win is structural, not a tuning delta.
Same-instance A/B (Moon-spin vs Moon-tuned-uring vs Redis 8.x control), GCE dedicated 4-vCPU, pinned disjoint cores, steal-gated, fresh-server best-of-5, loopback c=1 P=1, spin=40µs, n=3 instances per arch:
| arch | cmd | Moon busy-poll | Redis | ratio (3 instances) |
|---|---|---|---|---|
| ARM c4a Axion | SET | ~75.3k ops/s (13.3µs/op) | ~63.0k (15.9µs/op) | 1.193 / 1.196 / 1.198 |
| ARM c4a Axion | GET | ~77.2k ops/s (12.9µs/op) | ~63.9k (15.6µs/op) | 1.206 / 1.205 / 1.212 |
| x86 c3 Intel | SET | ~62.0k ops/s (16.1µs/op) | ~37.4k (26.7µs/op) | 1.657 / 1.663 / 1.647 |
| x86 c3 Intel | GET | ~63.2k ops/s (15.8µs/op) | ~38.2k (26.2µs/op) | 1.657 / 1.656 / 1.646 |
Sub-1% ratio spread across instances on both arches. Without busy-poll, Moon's best p=1 stance is
--io-driver epoll: 0.95× ARM / 1.06× x86 (same-instance A/Bs, 2026-07-03).
Multi-client (pinned-core VM probe): busy-poll also wins c=8 (+6.5%) and c=64 (+27.6%, p50 159→87µs) — under load the loop rarely parks, so the spin only fires where it helps. Idle cost: ~3% of a core (vs 1% stock) at the 40µs budget against 1ms timer parks.
Caveats: (1) judge busy-poll ONLY on pinned/dedicated cores — on unpinned shared-core hosts
(laptops, OrbStack defaults) the spinning thread displaces its own client and shows as a regression;
(2) claim scope is shards=1, loopback; (3) io_uring cannot busy-poll this way (CQE posting requires
owner-task participation — DEFER_TASKRUN, TWA_SIGNAL, and SQPOLL variants all measured worse;
experiment ledger in tmp/KV-FULLPROOF.md Round 2).
2.11 2026-08-10 RESP3 type-fidelity regression check (branch fix/resp3-type-fidelity, eff5a1e9, PR #463)¶
Not a performance claim — a no-regression gate. PR #463 adds a shape classification on the
command path (Resp3Shape, computed at enqueue for cross-shard commands) and a conversion at the
reply exit. Both are gated on proto >= 3; redis-benchmark speaks RESP2, so this measures the
cost of the gate, which is the thing that could regress every existing workload.
Method — and why the first attempt was discarded. A single full bench-compare.sh matrix
(200k requests, both arches, main vs branch) produced apparent Moon regressions of −16.3% (HSET),
−15.5% (LRANGE 100) and −11.4% (GET) on x86. Those were noise, not signal: Redis — unchanged
code, benchmarked in both legs — drifted by up to −27.0% on x86 and +11.3% on ARM between
the same two legs. When the control moves further than the subject, no per-row number is
interpretable (cf. §2.5).
The single pass was therefore replaced by an interleaved A/B: 5 repetitions alternating main → branch → main → branch, so thermal and neighbour drift hit both legs equally, 100k requests per point, Redis re-measured every repetition as an ongoing control. "Noise floor" below is the worst within-leg coefficient of variation across the four series for that row (Moon-main, Moon-branch, Redis-main, Redis-branch).
| arch | median Moon Δ (branch vs main) | median noise floor | rows outside noise |
|---|---|---|---|
| x86 c3-standard-8 | +0.00% | 5.99% | 0 / 18 |
| ARM t2a-standard-8 | +0.72% | 3.63% | 1 / 18 |
Grid: {SET, GET, INCR, LPUSH, SPOP, HSET} × p={1, 8, 64}, 50 clients, shards=1, --appendonly no
--disk-offload disable, Redis 7.0.15 with --save "" --appendonly no.
The one ARM row outside its floor is GET p=1 at +7.41% against a 5.33% floor — a gain, and not a claimable one: Redis on the same row moved +2.45% in the same direction, leaving ~5% residual against a 5.33% floor. Recorded rather than claimed.
Conclusion: no regression is detectable at this resolution on either architecture. The stronger statement — "there is no regression" — is not supported and is not made; what is supported is that any effect is smaller than a noise floor of 3.6% (ARM) / 6.0% (x86).
Caveats: (1) shared-tenant GCE instances, not pinned/dedicated — see §2.5 and the §2.10 pinning
caveat; (2) RESP2 path only, since redis-benchmark cannot negotiate RESP3 — the converted RESP3
path is covered for correctness by tests/resp3_type_fidelity.rs and the client-compat harness,
not for throughput; (3) shards=1, loopback.
3. Memory Efficiency¶
3.1 Baseline RSS (Empty Server)¶
| Server | RSS | Notes |
|---|---|---|
| Redis 8.6.1 | 7.0 MB | Single-threaded |
| moon (1 shard) | 7.0 MB | Lazy Lua VM + lazy replication backlog |
| moon (12 shards) | 15.7 MB | Per-shard overhead: ~0.7 MB |
3.2 Per-Key Memory (1-Shard, String Keys)¶
Measured with fresh server instances. redis-benchmark -r N for unique keys.
| Value Size | Keys Loaded | Redis/Key | moon/Key | Winner | Ratio |
|---|---|---|---|---|---|
| 32 B | ~63K | 118 B | 147 B | Redis | 0.80x |
| 256 B | ~63K | 412 B | 407 B | Tied | 1.01x |
| 1,024 B | ~63K | 1,879 B | 1,207 B | moon | 1.56x |
| 4,096 B | ~63K | 5,131 B | 4,352 B | moon | 1.18x |
At 500K keys:
| Value Size | Redis/Key | moon/Key | Winner | Ratio |
|---|---|---|---|---|
| 32 B | 118 B | 149 B | Redis | 0.79x |
| 256 B | 379 B | 379 B | Tied | 1.00x |
| 1,024 B | 1,786 B | 1,168 B | moon | 1.53x |
At 1M keys:
| Value Size | Redis RSS | moon RSS | Redis/Key | moon/Key | Winner |
|---|---|---|---|---|---|
| 32 B | 78.2 MB | 95.8 MB | 118 B | 147 B | Redis |
| 256 B | 231.5 MB | 234.4 MB | 372 B | 376 B | Tied |
| 1,024 B | 954.2 MB | 703.0 MB | 1,571 B | 1,153 B | moon |
3.3 Why moon Uses Less Memory at Larger Values¶
moon stores heap strings as HeapString(Vec<u8>) (24 bytes + data) instead of Redis's robj + SDS chain:
moon: CompactValue(16B) -> Box<HeapString> -> Vec<u8>(ptr+len+cap=24B) -> data
Total overhead: 16 + 8(box) + 24(vec) = 48 bytes + data
Redis: dictEntry(24B) -> robj(16B) -> SDS(header 8-17B + data) + jemalloc rounding
Total overhead: ~64-80 bytes + data
For small strings (<=12 bytes), moon uses SSO (Small String Optimization) — the value is stored inline in the 16-byte CompactValue struct with zero heap allocation. Redis still allocates robj + SDS for all strings.
3.4 TTL Memory Overhead¶
moon packs TTL as a 4-byte delta inside CompactEntry. Redis maintains a separate expires hash table with a full dictEntry (24 bytes) per expiring key.
| Server | TTL Implementation | Extra Memory Per Expiring Key |
|---|---|---|
| Redis | Separate expires dict |
~24 bytes (dictEntry) |
| moon | 4-byte delta in CompactEntry | 0 bytes (already included) |
3.5 Multi-Shard Memory (12 shards, 1M keys x 64B)¶
| Server | RSS |
|---|---|
| Redis | 107.6 MB |
| moon (12 shards) | 139.8 MB |
Per-shard overhead includes: DashTable segments, event loop state, SPSC channels (256 entries each), Notify handles, timers. This is the cost of the shared-nothing multi-core architecture.
4. Throughput¶
4.1 Single-Shard SET Throughput (P=16, c=50)¶
| Value Size | Redis SET/s | moon SET/s | Ratio |
|---|---|---|---|
| 32 B | 1,298,701 | 1,754,386 | 1.35x |
| 256 B | 1,219,512 | 1,639,344 | 1.34x |
| 1,024 B | 1,010,101 | 1,030,928 | 1.02x |
| 4,096 B | 540,541 | 571,429 | 1.06x |
4.2 Multi-Shard Peak Throughput (Monoio runtime)¶
| Config | moon | Redis | Ratio |
|---|---|---|---|
| 8-shard GET P=16 c=50 | 2.60M | 1.41M | 1.84x |
| 8-shard SET P=16 c=50 | 2.52M | 1.27M | 1.99x |
| 4-shard GET P=64 c=50 | 3.79M | 2.41M | 1.57x |
| 8-shard SET P=64 c=50 | 2.19M | 1.48M | 1.48x |
| 8-shard SET P=16 c=1000 | 2.12M | 1.20M | 1.76x |
4.3 String Substring Operations (1-shard, c=50, macOS)¶
| Command | Pipeline | Redis | moon | Ratio |
|---|---|---|---|---|
| GETRANGE | P=1 | 71,003 | 140,292 | 1.98x |
| SETRANGE | P=1 | 73,954 | 139,353 | 1.88x |
| GETRANGE | P=16 | 814,332 | 1,620,746 | 1.99x |
| SETRANGE | P=16 | 998,004 | 1,459,854 | 1.46x |
GETRANGE extracts a 13-byte substring from an 85-byte string. SETRANGE overwrites 5 bytes at offset 7. SETRANGE write-path advantage narrows at high pipeline depth due to per-op allocation overhead (zero-pad check, TTL preservation).
4.4 Scaling Efficiency (GET throughput vs 1-shard)¶
| Shards | Scaling Factor |
|---|---|
| 1 | 1.00x |
| 2 | 1.27x |
| 4 | 1.43x |
| 8 | 1.46x |
| 12 | 1.39x |
Scaling is sub-linear due to cross-shard SPSC dispatch overhead and shared loopback network bandwidth. Separate-machine benchmarks with dedicated NICs would show closer to linear scaling.
Refined 2026-06-15 (§2.8.4): on GCloud c3/t2a with a uniform single-key GET/SET workload at c=50, 1→8 shards is flat-to-slightly-negative (x86 GET p=64 holds ~4.7M, p=16 −2%; ARM −4–5%), not the +1.46× above. The positive scaling here reflects a non-uniform / higher-concurrency workload; for uniform cross-shard routing, single-shard is best (CLAUDE.md gotcha). Multi-shard wins come from pipeline/AOF parallelism and hash-tag co-location, not raw uniform-key fan-out.
4.5 2026-06-17 data-structure coverage (cross-arch, shards=1, p=1)¶
A wider bench-compare.sh pass (build 8238515) confirmed every core data structure tracks GET/SET at
p=1 (~0.79× Redis, TCP-RTT bound — see §6.2) with no structure-specific cliff: SET/GET/INCR 0.79×,
LPUSH/RPUSH/LPOP/RPOP 0.76–0.80×, LRANGE 100–600 0.78–0.80×, SADD/SPOP 0.80×, HSET 0.78×, ZADD 0.79×,
ZPOPMIN 0.72× (x86). ARM shows several structures at/above parity at p=1 (ZPOPMIN 1.10×, SADD 0.99×, HSET
0.97×). All cross decisively into Moon's favor under pipelining (§6.4). Detail:
docs/reviews/2026-06-17/WIDER-BENCH.md.
5. CPU Efficiency¶
5.1 CPU% and Throughput by Pipeline Depth (1-shard, 200K pre-loaded keys)¶
| Pipeline | Redis CPU% | moon CPU% | Redis RPS | moon RPS | RPS Ratio | CPU/100K-ops (Redis) | CPU/100K-ops (moon) |
|---|---|---|---|---|---|---|---|
| P=1 | 97.2% | 91.1% | 169K | 148K | 0.87x | 57.9% | 62.0% |
| P=8 | 100.0% | 3.3% | 1.14M | 1.11M | 0.97x | 8.8% | 0.29% |
| P=16 | 100.0% | 1.9% | 1.95M | 1.97M | 1.01x | 5.1% | 0.10% |
| P=64 | 43.9% | 1.9% | 2.42M | 4.13M | 1.71x | 1.8% | 0.05% |
At P=64, moon delivers 1.71x the throughput of Redis while using 23x less CPU.
5.2 Why moon Is More CPU-Efficient¶
- io_uring-style batch I/O — amortizes syscall overhead across multiple commands
- DashTable SIMD probing — 16-way parallel key matching with SSE2/NEON
- CompactEntry (24B) — cache-friendly vs Redis's 56-byte dictEntry + robj indirection
- Lock-free oneshot channels — eliminated 12% CPU from pthread_mutex contention
- CachedClock — eliminated 4% CPU from clock_gettime syscalls
- Software prefetch — overlaps DashTable segment fetch with hash computation
5.3 Profiling Breakdown (8-shard, P=16)¶
| Component | CPU% |
|---|---|
| Connection handler (Frame alloc, HashMap, Vec) | ~33% |
| Event loop + SPSC drain | ~12% |
| RESP parse + serialize (memchr SIMD, itoa) | ~11% |
| DashTable Segment::find (SIMD probing) | ~10% |
| Memory ops (memmove/memcmp) | ~6% |
| System (kevent) | ~2% |
6. Multi-Shard Scaling¶
6.1 Phase 40-43 Optimization Journey¶
| Phase | Fix | Impact on 8-shard GET |
|---|---|---|
| Before | Individual SPSC dispatch, .to_vec() copies, flume mutex oneshot | 0.52x Redis |
| 40 | Pipeline batch dispatch, buffer reuse | 1.10x Redis |
| 41 | Zero-copy .freeze() writes, borrow batching | 1.30x Redis |
| 42 | Inline dispatch for 1-shard GET/SET | Full parity |
| 43 | Lock-free oneshot, CachedClock | 1.84x Redis |
6.2 p=1 Performance (No Pipeline)¶
| Config | Ratio vs Redis |
|---|---|
| 1-shard SET | 1.02x |
| 1-shard GET | 0.95-1.02x |
| 8-shard SET | 1.04-1.11x |
At p=1, TCP loopback latency (~5000ns) dominates. Command processing (156ns) is 2.6% of total latency. Both servers hit the same network ceiling.
6.3 Connection Scaling¶
| Clients | Advantage |
|---|---|
| 1-10 | 1.93-3.27x moon (low contention, cache locality wins) |
| 50 | ~1.0x (parity) |
| 100-500 | 0.88-0.92x (async runtime overhead under contention) |
Optimal operating point: 10-50 clients per shard.
6.4 2026-06-17 cross-arch shard scaling (1 / 4 / 12, GCloud, build 8238515)¶
A wider sweep (scripts/bench-compare.sh at --shards 1/4/12 + bench-production.sh, Moon vs Redis 7.0.15,
x86 c3-standard-8 + ARM t2a-standard-8, AOF-off both sides). The clean result: Moon's advantage is
pipeline depth, not shard count. GET ratio (Moon/Redis), x86 — flat across shard count:
| pipeline | shards=1 | shards=4 | shards=12 |
|---|---|---|---|
| p=1 | 0.79× | 0.79× | 0.82× |
| p=16 | 1.05× | 0.94× | 0.96× |
| p=64 | 1.88× | 1.88× | 1.91× |
| p=128 | 2.84× | 2.92× | 2.81× |
SET wins similarly at depth (p=64 1.85× s1, declining to 1.54× s12 — cross-shard write dispatch). Adding
shards does not help, and for uniform single-key non-pipelined work it hurts: at 12 shards the
production p=1 GET/SET collapse to 0.46–0.51× Redis (vs ~0.79× at 1 shard) on both arches — the
cross-shard SPSC dispatch tax (confirms the "single-shard is best for non-pipelined" gotcha, quantified). ARM
mirrors x86 (SET p=64 1.96×/1.86× at s1/s12; GET p=128 2.74×/2.42×). Guidance: --shards 1 for uniform
non-pipelined workloads; add shards only for pipelined / AOF / hash-tag-co-located workloads. MSET
degrades with shard count (0.93×→0.61× as 1→12) as its keys scatter cross-shard — {hash-tag} co-location
restores it. Detail: docs/reviews/2026-06-17/WIDER-BENCH.md.
Clarification (2026-07-02 KV deep review). The 0.46–0.51× p=1 figure above is from
bench-production.sh, which changes three things at once besides shard count: distributed keys (-r, real cross-shard scatter — vsbench-compare.sh's single hot__rand_key__), larger values (512B–4KB), and higher client counts (-c 100/200on the INCR rows). It is a throughput artifact of {distributed keys × high concurrency × value size × multi-key scatter}, not a clean shard-count signal — the cross-shard hop itself is ~10µs (v2-2 bare-metal), ~2% of the ~460µs p=1 baseline. The controlledbench-compare.shsweep in the table above (only shard count varies) is flat at p=1 (0.79→0.82×). Read 0.46× as "production-shaped multi-key under concurrency," not "the cross-shard hop costs 2×."
6.5 2026-07-03 shards × busy-poll sweep (--measure-shards, build 74de849)¶
Same-instance rigor (pinned, steal-gated, fresh-server best-of-3, strict -r 1M keyspace so
shards=4 really scatters), c4a-standard-8 + c3-standard-8: Moon shards {1, 4} × --io-busy-poll-us
{0, 40} vs one canonical single-threaded Redis; shards on cores 0–3, 3-thread client on 5–7.
Ratio = Moon/Redis:
| cell (cmd ≈ SET/GET) | ARM stock | ARM spin40 | x86 stock | x86 spin40 |
|---|---|---|---|---|
| s1 c1 P1 (single-op latency) | 1.02 | 1.21 / 1.25 | 1.06 | 1.42 / 1.43 |
| s4 c1 P1 (cross-shard hop) | 0.70 | 0.85 / 0.88 | 0.64 | 0.88 / 0.91 |
| s4 c8 P1 (scattered, no pipeline) | 0.44 | 0.50 / 0.58 | 0.41 / 0.45 | 0.64 / 0.56 |
| s4 c8 P16 SET (Redis server-bound) | ≥2.0× | ≥2.0× | ~1.0 (capped) | ≥2.0× |
Readings:
- The §2.10 busy-poll p=1 win replicates on the 8-vCPU shape (5th ARM + 4th x86 instance).
- Busy-poll recovers a large fraction of the cross-shard hop (the target shard normally sleeps;
spin removes that wake): s4 single-conn improves +22% ARM / +38–42% x86 — but shards=4 still
loses every non-pipelined cell. The SPSC dispatch cost dominates once keys scatter.
- Guidance unchanged, now sharper: --shards 1 --io-busy-poll-us 40 is the p=1 configuration
that beats Redis outright; add shards only for pipelined / AOF / hash-tag-co-located workloads.
- ⚠ Deep-pipeline cells (P16/P64) on this co-located pinned topology are mostly client-saturated
— both engines plateau at ~1.2M ops/s with identical durations (the 3-thread pinned client is the
bottleneck), so their ≈1.00 "ties" are ceilings, not measurements. Readable exceptions: Redis SET
P16 is server-bound at ~599k (Moon ≥2.0× there, understated), and x86 s4 spin P64 SET burst past
the plateau to 1.74M (1.46×). For whole-machine peak throughput, §2.8/§6.4's methodology (client
gets the full core budget) remains the reference.
- ⚠ SUPERSEDED for c≥8 by §6.6: the s4 c8 P1 collapse in this table was a reply-spin convoy
bug, fixed on perf/v3-3-p1-hotpath. Multi-shard now wins every multi-connection cell.
6.6 2026-07-04 multi-connection WIN — convoy fix + slot-unified replies (branch perf/v3-3-p1-hotpath, fd13f03)¶
The §6.5 s4-c8 collapse (0.44–0.64×) was diagnosed as a reply-spin convoy: the cross-shard
reply busy-poll ran synchronously on the shard thread with a gate that admitted two waiters, so
at 2 conns/shard a spinning connection starved both its sibling and the shard's own SPSC drain
(circular cross-shard stall). Fix chain, each same-instance A/B-validated on GCE:
solo-conn spin gate (795c4f0, c8 P1 2.75× vs pre-fix) → monoio reply path unified on the
zero-allocation ResponseSlotPool (fd13f03, c8 P1 +11–14% on top). Ratio = Moon/Redis,
s4, P1, busy-poll 40, 4-thread pinned client, best-of-5 × 500k requests, n=2 instances on x86:
| cell | ARM c4a-standard-8 | x86 c3-standard-8 |
|---|---|---|
| c8 GET / SET | 1.57× / 1.71× | 1.9× / 2.0× (same-instance; ≥1.6× vs Redis's best client config) |
| c64 GET / SET | 2.50× | 2.50× (same-instance; ≥1.86× best-vs-best) |
| c1 (structural ceiling = s1 latency) | 0.93–0.96 | 0.91–0.94 |
Readings:
- Guidance updated: --shards 1 remains best for 1–4 unpipelined connections, but from
8 concurrent connections up, --shards 4 beats Redis 1.5–2.5× even without pipelining.
The pipeline moat (§6.5, §7) is unchanged. See docs/guides/tuning.md.
- s4 c1 P1 is hop-bound, not fixable by tuning: a perfect message-passing multi-shard equals
s1 latency (its ceiling is §2.10's s1 ratio). Executing foreign reads locally (shared-read data
plane) is the only lever; deferred as future work.
- ⚠ Instrument notes: raw files tmp/hp-{c4a,c3}-l3b-ab.txt, tmp/hp-c3-l3b-ab2.txt,
tmp/redis-threads-control.txt. redis-benchmark --threads 4 lowers Redis's x86 c8/c64
readings 15–20% vs --threads 3 (Moon is thread-insensitive; ARM unaffected) — the x86 row
therefore also states the conservative ratio against Redis's best client config. Unpipelined
c64 readings quantize to attractors (333,333 / 500,000 = requests ÷ ms-quantized duration);
values are best-of-5 per cell.
7. Persistence (AOF) Performance¶
7.1 With AOF Everysec, Advantage Grows¶
| Pipeline | SET ops/s (moon) | vs Redis (no AOF) | vs Redis (AOF everysec) |
|---|---|---|---|
| P=1 | 146K | 0.95x | 0.95x |
| P=8 | 1,117K | 1.68x | 1.68x |
| P=16 | 1,887K | 1.90x | 2.21x |
| P=32 | 2,469K | — | 2.52x |
| P=64 | 2,778K | 1.80x | 2.75x |
7.2 Why Persistence Makes moon Faster (Relatively)¶
| Aspect | Redis | moon |
|---|---|---|
| AOF architecture | Global append-only file, single writer thread | Per-shard WAL files, no global lock |
| Hot-path cost | Buffer + background rewrite | buf.extend_from_slice() (~5ns) |
| Flush | Background fsync | Batch write_all every 1ms tick |
| Fsync | Dedicated bio thread | Separate timer, every 1 second |
| Under P=64 | Global AOF becomes serialization point | Per-shard WAL scales linearly |
7.3 2026-07-08 Durability write-path campaign (PRs #238–#242)¶
Goal: close every remaining Moon-vs-Redis deficit on the AOF-on write path so Moon is at parity-or-better under all durability policies, not just --appendonly no.
Setup: GCE c3-standard-8 (Sapphire Rapids, 8 vCPU, pd-ssd), Ubuntu 24.04, Redis 7.0.15, Moon --shards 2 (its default multi-core posture on 8 vCPU) vs Redis stock single-threaded. redis-benchmark -t set -c 8 -n 300000 -r 100000 -d 64 [-P 16]. Fresh server + data dir per scenario, 3 alternated reps (engine order flips each rep to cancel warm-up/thermal drift), each run provenance-probed (thread-name scan + strace -c confirming the code path under test). Numbers are 3-rep means.
7.3.1 Results — the four deficits, before → after¶
| Policy / workload | Before campaign | After (this branch) | Redis | vs Redis after |
|---|---|---|---|---|
always SET P16 |
5,680 | 39,621 → 40,084 | 43,898 | 0.91× |
always SET P1 (control) |
~3,180 | 3,114 | 3,153 | parity (fsync-device-bound) |
everysec SET P16 |
605,317 | 788,717 | 597,563 | 1.32× WIN |
everysec SET P1 |
116,619 (0.80×) | 134,158–135,280 | 135,888 | 0.99× parity |
nodur SET P1 (control) |
135,795 | 135,319 | 132,185 | 1.02× (unchanged) |
| Pub/sub fan-out delivery | 438 msg/s (drops ≈100%) | 5.09M msg/s (0 drops) | 4.89M msg/s | 1.04× WIN |
always P16 moved in two steps: PR #239 (per-command awaited fsync → per-batch group commit) took it 0.12× → 0.85×; PR #242 (coalesced batch write) took it 0.85× → 0.91×.
7.3.2 What each fix changed¶
| PR | Deficit | Root cause (measured) | Fix |
|---|---|---|---|
| #238 | always shard stalls |
WAL v3 fdatasync ran on the shard event-loop thread — every fsync froze SPSC drain + conn I/O + CDC |
Off-loop WalSyncAgent (per-shard std::thread, fd-dup requests, durable-LSN watermark); +18% RPS, p99/p999 −40–60% under always |
| #239 | always P16 0.12× |
Handler awaited one fsync ack per pipelined command; a 16-deep pipeline paid 16 serialized fsync RTTs/conn while Redis fsyncs once per event-loop iter | Local writes join the per-batch group commit (fire-and-forget send_append_group + ONE fsync_barrier per batch); 0.12× → 0.85× (6.98×) |
| #240 | Pub/sub delivery 438 msg/s | One write(2) per delivered message; under fan-out flood the 256-slot subscriber queue stayed full → near-total drops |
Coalesce the queued burst (try_recv drain, 64 KB cap) into ONE write_all; 438 → 5.09M msg/s, zero drops |
| #241 | everysec P1 0.80× |
AOF writer parked in flume::recv_timeout, so every producer try_send paid a futex WAKE on the shard thread — strace -c: 149,718 futex calls (63% of shard-thread syscall time) in an 8 s SET run vs 2 under nodur |
Under everysec/no the writer polls park-free (try_recv + adaptive sleep, wait/16 clamped 500 µs–50 ms) so producer sends stay pure userspace atomics; 149,718 → 96 futex calls, +15% RPS → 0.99× parity. always keeps the parked recv (ack latency is client-visible RTT) |
| #242 | always P16 0.85× residual |
Writer was write-syscall-bound, not fsync-bound: strace -c on aof-writer-0 showed 127,812 write(2) calls / 1.25 s vs 2,144 fdatasync / 0.20 s (one write per record on the raw unbuffered file; per-shard framed path used a header+body pair). Redis batches ~120 records/write via aof_buf |
Coalesce each group-commit batch into ONE contiguous write_all before its single fsync (reusable buffer, 1 MB high-water cap); 0.85× → 0.91×, and everysec P16 (shares the write path) → 1.32× |
Durability invariant held throughout. Every change preserves fsync-before-ack (H1): the AOF writer channel is ordered, so an acked zero-length AppendSync barrier proves every prior Append is on disk; a failed batch acks every waiter an error (never a silent +OK) and latches the torn stream. Re-verified after each change with crash_matrix_per_shard_aof --ignored (SIGKILL under appendfsync always recovers 100% of acked writes): 3/3 green.
7.3.3 Reading the numbers¶
alwaysP1 is a hardware floor, not a Moon limit — ~3.1–3.4k for all three engines. Each write is one fsync RTT to pd-ssd; the disk, not the server, sets the rate. Parity here is the best any correct implementation can do.alwaysP16 residual 0.91× — both engines are fsync-device-bound at pipeline depth; the last ~9% is the per-batch barrier ack round-trip, and two shards' fsyncs serialize at the device rather than parallelizing. Diminishing returns.everysecP16 1.32× WIN — no per-batch fsync ceiling here, so the coalesced-write + park-free-poll savings surface directly as throughput.- Base column caveat — the
everysec/always"before" figures come from the pre-campaign tip; the P1everysecbase (116,619) predates PR #241, which is why the after-figure jumps despite the write-path work.
Full method notes and per-rep tables: tmp/MOON-VS-REDIS-DURABILITY.md, tmp/WALV3-OFFLOOP-FSYNC.md.
8. Production Workload Patterns¶
From scripts/bench-production.sh (10 scenarios):
| Scenario | Description | moon vs Redis |
|---|---|---|
| Session store | 80% GET / 15% SET, 512B values | 1.24x |
| Rate limiting | INCR with 100-200 clients | 1.15x |
| Leaderboard | ZADD + ZRANGEBYSCORE | 1.06-1.25x |
| App caching | 1KB-4KB values, MSET batch | 1.10-1.27x |
| Job queue | LPUSH/RPOP producer-consumer | 1.06x |
| User profiles | HSET, HGET | 1.10x |
| Data sizes | 8B to 64KB payloads | 1.10-1.27x |
| Pipeline depth | P=1 to P=128 | 1.02-1.67x |
Collection commands (LPUSH, HSET, ZADD) at P=64 are 1.06-1.25x Redis because execution time (200-400ns) dominates parsing overhead (83ns), and DashTable + CompactEntry + B+ tree genuinely outperform Redis's dict + skip list for mutations.
8.1 Data Size Advantage¶
moon wins across ALL payload sizes for both SET and GET:
| Value Size | GET Advantage | SET Advantage |
|---|---|---|
| 8 B | 1.10x | 1.12x |
| 256 B | 1.15x | 1.18x |
| 4 KB | 1.20x | 1.22x |
| 64 KB | 1.27x | 1.25x |
Larger values amplify the io_uring zero-copy and writev scatter-gather advantage.
9. Latency¶
9.1 p50 Latency (8-shard)¶
| Metric | Redis | moon | Improvement |
|---|---|---|---|
| p50 latency | 0.26-0.33 ms | 0.031 ms | 8-10x lower |
Multi-core parallelism reduces per-shard queue depth. The median request sees less waiting time. This is the real production advantage for latency-sensitive workloads.
10. Vector Search¶
Date: 2026-04-15 Dataset: 50K vectors, 384 dimensions (MiniLM-L6-v2 semantic embeddings), COSINE distance Index: HNSW (M=16, EF_CONSTRUCTION=200), TurboQuant 8-bit
10.1 Throughput (GCloud c3-standard-8, x86_64)¶
| Operation | moon | Notes |
|---|---|---|
| Vector insert | 8,200 vec/s | HSET with 384d float32, auto-indexed |
| Search QPS | 12,700 QPS | FT.SEARCH, K=10, brute-force mutable segment |
10.2 Throughput (GCloud t2a-standard-8, ARM64)¶
| Operation | moon | Notes |
|---|---|---|
| Vector insert | 7,700 vec/s | HSET with 384d float32, auto-indexed |
| Search QPS | 7,100 QPS | FT.SEARCH, K=10 |
10.3 Recall¶
| Configuration | Recall@10 |
|---|---|
| FP32 HNSW (384d, MiniLM) | 0.96+ |
| TQ8 after compact | 0.92 |
| TQ4 (384d) | Not recommended — concentration of distances at low dims |
TQ4 is designed for 768d+ workloads. For 384d and below, use TQ8 or FP32 HNSW.
10.4 vs Competitors (OrbStack, MiniLM 384d)¶
| Metric | moon | Redis (RediSearch) | Qdrant |
|---|---|---|---|
| Insert/s | 31,000 | 4,000 | 6,600 |
| Search QPS | 1,400 | 3,800 | 982 |
| Recall@10 | 0.92 | 0.95 | 0.96 |
| Insert speedup | 7.7x Redis | 1x | 1.7x |
Moon's insert pipeline is 7.7x faster than RediSearch due to zero-copy HSET + in-memory auto-indexing. Search QPS with brute-force mutable segment is competitive; HNSW immutable segment search is faster after FT.COMPACT.
10.5 2026-06-16 Concurrent throughput vs RediSearch (GCloud, db61973)¶
Re-measured under 8 concurrent clients (not single-connection pipelined as §10.1–10.2) against a live RediSearch
(redis/redis-stack-server Docker), on c3-standard-8 (x86) + t2a-standard-8 (ARM). 50K × 384d clustered vectors
(mixture-of-Gaussians → meaningful recall), COSINE, KNN-10, recall@10 vs exact numpy ground truth.
| metric | x86 Moon | x86 RediSearch | ARM Moon | ARM RediSearch |
|---|---|---|---|---|
| Insert/s | 25,133 | 3,989 | 35,937 | 1,799 |
| Search QPS (HNSW, 8 threads) | 552 | 9,088 | 505 | 6,842 |
| Search p50 | 14.2 ms | 0.71 ms | 15.5 ms | 1.17 ms |
| Recall@10 (HNSW) | 0.858 | 0.961 | 0.858 | 0.961 |
Honest reframing of §10.1's 12.7K QPS (which was single-connection pipelined, Moon-only): under 8-way
concurrency against a live RediSearch, Moon's vector insert is 6–20× faster (zero-copy HSET + auto-index) but
RediSearch's vector search is ~16× higher QPS at higher recall (0.96 vs Moon's 0.86 — Moon's default
auto-quantization SQ8/TQ trades recall for memory at 384d; CLAUDE.md notes 384d quantization loses recall). RediSearch
has no FT.COMPACT (auto-builds HNSW on insert), so its row is already its HNSW. Detail:
docs/reviews/2026-06-16/4FEATURE-BENCH.md.
10.6 2026-06-17 verified re-run (v3-1/v3-2 build 8238515) — no regression¶
Re-ran the §10.5 protocol on the verified post-v3-1/v3-2 build to confirm Vector is untouched. Identical
within VM variance — Vector engine was not changed by either milestone (= the planned v3-3-vector-kv-polish
target). Detail: docs/reviews/2026-06-17/4FEATURE-VERIFIED.md.
| metric | x86 Moon | x86 RediSearch | ARM Moon | ARM RediSearch |
|---|---|---|---|---|
| Insert/s | 28,136 | 3,750 | 28,343 | 1,906 |
| Search QPS (HNSW, 8 threads) | 473 | 9,143 | 540 | 6,926 |
| Recall@10 (HNSW) | 0.858 | 0.961 | 0.858 | 0.961 |
Moon insert 7.5× (x86) / 14.9× (ARM) faster; RediSearch search ~16× higher QPS at higher recall (the SQ8/TQ-at-384d recall trade-off). Unchanged from §10.5 — no v3-1/v3-2 regression.
(Note: the search-QPS deficit above predates the vector-search optimization branch (PR #214, insert 24–38×, matched-recall gap 16×→~1.3×) and the HQ-1 exact-rerank sidecar — see §10.7 below for post-optimization numbers vs Qdrant.)
10.7 2026-07-07 vs Qdrant (OrbStack Linux VM, Docker Qdrant, same box)¶
50K × 384d clustered gaussian (unit-normalized), COSINE, KNN10, 500 queries with exact
ground truth, redis-py / qdrant-client (REST), 8-thread QPS. Both engines at default HNSW
params; recall@10 is matched (≥0.999 both) so QPS compares at equal quality. Branch
feat/graph-engine-wave2 HEAD.
| metric | Moon | Qdrant | ratio |
|---|---|---|---|
| Ingest rate | 65,695 vec/s (searchable immediately, brute tier) | 7,350 vec/s accepted / 6,399 vec/s to index-green | 8.9× |
| Time to HNSW-quality serving | 28.7 s (FT.COMPACT, incl. 6 s grace) |
7.8 s (optimizer green) | 0.27× |
| Search QPS (HNSW, 8 threads) | 3,092 | 1,223 | 2.53× |
| Search p50 / p99 | 2.60 / 4.39 ms | 6.05 / 14.66 ms | 2.3× / 3.3× |
| Recall@10 | 0.9992 | 0.9998 | parity |
Moon wins ingest 8.9× and matched-recall search 2.5×; Qdrant reaches HNSW-tier serving faster after bulk load (Moon serves immediately from the brute tier during that window — recall 0.759 there, the documented TQ-at-384d quantized-brute trade-off; low brute QPS at 50K is expected O(N) scan). Caveats: qdrant-client REST transport (gRPC would improve Qdrant's client-side latency somewhat); shared-host VM — ratios are the signal, absolutes are indicative.
10.8 2026-07-07 vs Qdrant — GCE validation (dedicated instances, x86 + ARM)¶
Same workload and harness as §10.7, re-run on dedicated GCE instances to validate the
same-box VM ratios on real cloud hardware: c3-standard-8 (Xeon Platinum 8481C, x86_64)
and t2a-standard-8 (Neoverse-N1, aarch64), us-central1-a. Fresh native build of branch
HEAD 852d53a on each instance; Docker Qdrant on the same box.
| metric | x86 Moon | x86 Qdrant | ARM Moon | ARM Qdrant |
|---|---|---|---|---|
| Ingest rate (vec/s) | 34,121 (searchable immediately) | 3,383 accepted / 3,167 to green | 26,034 (searchable immediately) | 2,383 accepted / 2,273 to green |
| Time to HNSW-tier serving | 27.2 s | 15.8 s | 47.6 s | 22.0 s |
| Search QPS (HNSW, 8 threads) | 2,097 | 613 | 1,422 | 531 |
| Search p50 / p99 (ms) | 3.72 / 5.61 | 11.98 / 23.88 | 5.50 / 7.98 | 13.75 / 26.11 |
| Recall@10 | 0.9992 | 1.0000 | 0.9992 | 0.9998 |
Moon ingests 10.1× (x86) / 10.9× (ARM) faster and serves matched-recall search 3.4× (x86) / 2.7× (ARM) faster — confirming §10.7's VM reading (2.53×) on dedicated cloud hardware. Qdrant keeps its time-to-index-green edge after bulk load (Moon serves from the brute tier in that window). Recall is computed against exact ground truth (full-precision brute-force top-10 over all 50K vectors per query); both engines ≥0.999, within 0.0008 of each other, and Moon's 0.9992 reproduces bit-identically across all three environments (VM, GCE x86, GCE ARM).
10.9 2026-07-08 time-to-index-green: parallel HNSW build + insert-path trigger — Moon now beats Qdrant (GCE, x86 + ARM)¶
§10.8's one losing metric fixed (commit 061c73cb, same instances/harness). Instrumentation
showed 99.3% of FT.COMPACT wall was a single-threaded HNSW insert loop, compounded by a
Linux affinity trap (threads spawned from core-pinned shard threads inherit the single-core
mask — available_parallelism() returned 1, silently serializing the "parallel" path AND
sizing the background-compactor pool to one worker) and by the auto-compact trigger living
only on the search path (a pure bulk load never compacted until the first FT.COMPACT).
Fixes: shared-graph concurrent HNSW builder (per-node locks + connectivity repair, builds
≥10K vectors, ~88% scaling efficiency), affinity-independent system_parallelism() with
explicit worker re-pinning, and an HSET-path compaction trigger (builds start + install
during ingest).
Load → HNSW-tier serving (50K × 384d bulk load, then immediate FT.COMPACT, no measurement traffic):
| x86 before | x86 after | x86 Qdrant | ARM before | ARM after | ARM Qdrant | |
|---|---|---|---|---|---|---|
| load → green | ~22.7 s | 9.9 s | 15.7 s | ~43.5 s | 9.5 s | 22.2 s |
Moon now reaches HNSW-tier serving 1.6× (x86) / 2.3× (ARM) faster than Qdrant — every §10.8 metric is now a Moon win. Full-workload re-run on the same boxes:
| metric | x86 Moon | x86 Qdrant | ARM Moon | ARM Qdrant |
|---|---|---|---|---|
| Ingest rate (vec/s) | 19,792 | 3,408 accepted | 24,519 | 2,357 accepted |
| Search QPS (HNSW, 8 threads) | 2,266 | 626 | 1,480 | 532 |
| Search p50 / p99 (ms) | 3.55 / 4.04 | 11.64 / 24.12 | 5.37 / 6.20 | 13.64 / 26.74 |
| Recall@10 | 0.9982 | 1.0000 | 0.9980 | 0.9998 |
Honest trade-offs: (1) ingest rate drops vs §10.8 (34K→20K x86) because HNSW builds now run concurrently with ingest — the same trade Qdrant makes (its 3.4K/s accept rate IS its indexing); Moon still ingests 5.8–10.4× faster. (2) recall@10 dips 0.9992 → 0.998x — the index now serves from 3 segments instead of one (multi-segment beam truncation), a ~0.001 recall cost for the 2.3–4.6× faster time-to-green; still within 0.002 of Qdrant.
10.10 2026-07-08 ANN-benchmarks 4-way: Moon vs RediSearch vs Qdrant vs turbovec (GCE ARM, 1.18M/1M vectors)¶
First campaign on the standard ANN-benchmarks datasets with bundled ground truth
(previous sections used synthetic/MiniLM corpora ≤50K). Full recall/QPS trade-off curves
via the new runtime FT.CONFIG SET <idx> EF_RUNTIME (commit 0afac2c4) — one ingest,
one settled index, N query points.
Environment: GCE t2a-standard-8 (ARM Ampere, 8 vCPU, us-central1-a), Moon
0afac2c4 (feat/vector-fastscan, -C target-cpu=native, monoio, --shards 8
--appendonly no --disk-offload disable --max-unflushed-immutable-segments 0);
RediSearch = redis/redis-stack-server:latest (Docker, HNSW M=16 EF_CONSTRUCTION=200);
Qdrant = qdrant/qdrant:latest (Docker, default HNSW, wait-for-optimizer green);
turbovec = github.com/RyanCodrai/turbovec (in-process Rust/Python TurboQuant flat index).
Harness: redis-py / qdrant-client / native API, 1000 test queries, recall@10 vs bundled
ground truth, qps1/qps8 = 1/8 client threads (no pipelining). Moon "settled" = ingest →
green → VACUUM VECTOR merge loop to 1 segment/shard (8 total).
glove-200-angular — 1,183,514 × 200d, COSINE, k=10¶
| system | config | recall@10 | qps (1 client) | qps (8 clients) |
|---|---|---|---|---|
| Moon (SQ8, settled) | ef=16 | 0.648 | 1448 | 2907 |
| ef=64 | 0.827 | 888 | 1899 | |
| ef=128 | 0.887 | 612 | 1309 | |
| ef=256 | 0.933 | 400 | 876 | |
| RediSearch | ef=16 | 0.496 | 2328 | 3618 |
| ef=64 | 0.694 | 1202 | 1704 | |
| ef=128 | 0.771 | 816 | 1074 | |
| ef=256 | 0.836 | 524 | 602 | |
| Qdrant | ef=16 | 0.570 | 410 | 574 |
| ef=64 | 0.768 | 335 | 544 | |
| ef=256 | 0.887 | 214 | 426 | |
| turbovec | 4-bit | 0.875 | 63 | 62 |
| 2-bit | 0.625 | 105 | — |
Iso-recall verdicts (the only fair read):
- vs RediSearch @ ~0.83 recall: Moon 888/1899 vs 524/602 — 1.7× / 3.2× win. (RediSearch's 2328-qps ef=16 row is at 0.50 recall — not a comparable operating point.)
- vs Qdrant @ ~0.887 recall: Moon 612/1309 vs 214/426 — 2.9× / 3.1× win.
- vs turbovec @ ~0.875 recall: Moon ~700+/1500+ vs 63/62 — >10× win; the flat scan is O(N) and collapses at 1.18M vectors.
Time-to-ready (same dataset): Moon ingest 165 s → green (HNSW-tier, 0.85+ recall available) at 277 s; RediSearch 1498 s; Qdrant ~390 s. Moon's fully-settled state (merge to 1 segment/shard) took a further ~61 min — see trade-offs below.
glove subset 100K (turbovec's native scale), COSINE¶
k=64 (turbovec's README config):
| system | config | recall@64 | qps1 | qps8 |
|---|---|---|---|---|
| Moon (SQ8, settled) | ef=64 | 0.886 | 647 | 1318 |
| ef=128 | 0.952 | 524 | 1103 | |
| ef=512 | 0.998 | 272 | 639 | |
| turbovec | 4-bit | 0.895 | 760 | 781 |
| 2-bit | 0.663 | 1191 | 1243 |
k=10:
| system | config | recall@10 | qps1 | qps8 |
|---|---|---|---|---|
| Moon (SQ8, settled) | ef=16 | 0.765 | 1787 | 3211 |
| ef=64 | 0.930 | 1076 | 2286 | |
| ef=256 | 0.995 | 527 | 1139 | |
| turbovec | 4-bit | 0.877 | 787 | 802 |
At k=10 Moon wins both axes (higher recall AND higher qps1, 2.9× qps8). At k=64 the in-process flat scan edges Moon on single-client qps (no network hop, cost amortized over large k) while Moon wins 8-client throughput 1.7× and is the only one that can reach >0.9 recall. turbovec gets no concurrency scaling (760 → 781).
gist-960-euclidean — 1,000,000 × 960d, L2, k=10¶
| system | config | recall@10 | qps1 | qps8 |
|---|---|---|---|---|
| Moon (SQ8, settled) | ef=16 | 0.816 | 995 | 2077 |
| ef=64 | 0.962 | 528 | 1085 | |
| ef=256 | 0.994 | 216 | 472 | |
| Qdrant | ef=16 | 0.624 | 264 | 390 |
| ef=64 | 0.861 | 215 | 371 | |
| ef=256 | 0.965 | 134 | 314 | |
| RediSearch | ef=16 | 0.486 | 1242 | 1827 |
| ef=64 | 0.757 | 591 | 798 | |
| ef=256 | 0.927 | 246 | 270 |
(turbovec omitted: IP/COSINE-only library, and its O(N) flat scan is non-viable at 1M.)
Iso-recall verdicts — Moon wins every band:
- @ ~0.86: Moon (ef≈24, interp.) ~4× / ~5× Qdrant's 215/371.
- @ ~0.96: Moon 528/1085 vs Qdrant 134/314 — 3.9× / 3.5×.
- @ ~0.93: Moon at higher recall (0.962 @ 528/1085) vs RediSearch 0.927 @ 246/270 — 2.1× / 4.0×.
- Moon is the only system measured past 0.99 recall (0.994 @ 216/472 — still faster than both competitors' ~0.93–0.965 operating points).
Time-to-ready (gist): Moon ingest 396 s → green 437 s; RediSearch 2871 s; Qdrant ~620 s. Moon settle (merge to 1 segment/shard) took a further ~97 min — the merge-parallelism gap noted below.
⚠ Moon TQ4 on gist recorded recall 0.002–0.003 — a bug, not noise. TQ's ADC ranked
L2 by sphere_dist·‖a‖², which is only rank-valid on the unit sphere; gist is
unnormalized L2 and the estimator collapses. Fixed in this branch (14a07025): all
TQ scoring paths reconstruct ‖a−q‖² = (‖a‖−‖q‖)² + ‖a‖‖q‖·d̂², and FT.CREATE L2
indexes now default to SQ8 (explicit TQ+L2 warns). GCE re-verification with
explicit TQ4 post-fix (same instance/harness, unsettled index so recall-only):
ef=16 0.784, ef=64 0.942, ef=256 0.986* — a ~350× recovery, within ~0.03 of
SQ8 (4-bit direction precision at 960d). The main table above is the SQ8 run.
Findings that changed Moon along the way¶
EF_RUNTIMEwas FT.CREATE-frozen → recall/QPS curves needed one full index rebuild per point. Now runtime-tunable viaFT.CONFIG SET(also fixed FT.CONFIG SET being silently local-shard-only under monoio at shards>1).- MA1 write-stall guard fires on TOTAL immutable segments (>20), not unflushed
ones — a 1M+ bulk load sits above the threshold while merges lag, throttling
ingest to ~190 vec/s (24× slowdown) on idle hardware. Workaround
--max-unflushed-immutable-segments 0during bulk loads; semantics fix pending. - Unmerged segments multiply query cost: per-query work ≈ shards × segments × ef.
Pre-settle (57 segments) the same index ran 4–5× slower at the same ef.
VACUUM VECTORmerges to 1 segment/shard but is local-shard-only over the wire and took ~56 min for 1.18M × 200d (and ~97 min for 1M × 960d) at only ~1.2 cores — GraphUnion merge parallelism is the next optimization target. - Multi-segment search unions independent beams → higher recall ceiling at equal ef (unsettled ef=256: 0.9865 vs settled 0.933). The settled index needs ef 512+ to reclaim the >0.95 band — at far higher qps than the unsettled equivalent.
11. Graph Engine¶
Date: 2026-04-15 Dataset: 2K nodes, 6K edges, sequential redis-cli commands Engine: CSR (Compressed Sparse Row) + SlotMap + Cypher subset
11.1 Throughput (GCloud c3-standard-8, x86_64)¶
| Operation | QPS | Notes |
|---|---|---|
| Node/Edge insert | 294/s | GRAPH.ADD via redis-cli (sequential, TCP overhead) |
| 1-hop neighbor query | 303/s | GRAPH.NEIGHBORS |
| Cypher query | 292/s | GRAPH.QUERY with pattern matching |
| CSR lookup (internal) | 923 ps/edge | Sub-nanosecond after FT.COMPACT builds CSR |
11.2 Throughput (GCloud t2a-standard-8, ARM64)¶
| Operation | QPS |
|---|---|
| Node/Edge insert | 216/s |
| 1-hop neighbor query | 239/s |
| Cypher query | 228/s |
11.3 vs FalkorDB (OrbStack)¶
| Metric | moon | FalkorDB |
|---|---|---|
| Cypher QPS | 2.4x | 1x |
| Native API QPS | 19x | N/A |
| Populate (bulk insert) | 23x | 1x |
Moon's shared-nothing per-shard graph with CSR compaction provides sub-nanosecond edge traversal after compaction. The native GRAPH.* API avoids Cypher parsing overhead for simple operations.
11.4 2026-06-16 vs FalkorDB (GCloud, 8 concurrent clients)¶
5K nodes, 15K edges. Moon native GRAPH.CREATE/ADDNODE/ADDEDGE; FalkorDB Cypher. redis-py, 8 threads.
| metric | x86 Moon | x86 FalkorDB | ARM Moon | ARM FalkorDB |
|---|---|---|---|---|
| Build ops/s | 25,333 | 962 | 12,592 | 605 |
| 1-hop qps (Moon native / Falkor Cypher) | 5,671 | 4,739 | 4,474 | 3,794 |
| 1-hop p50 | 1.12 ms | 1.53 ms | 1.54 ms | 1.94 ms |
| Cypher 1-hop / 2-hop qps | 40† / 13† | 4,739 / 4,459 | 28† / 9† | 3,794 / 3,639 |
Moon native builds 21–26× faster and its native 1-hop neighbor lookup edges out FalkorDB Cypher. † But Moon's
Cypher MATCH (a {id:N}) does not filter on an inline node-property predicate — it full-scans the label
(returns ~all 15K edges vs FalkorDB's correctly-filtered 4), so Moon Cypher point/2-hop queries are slow whole-graph
scans. Use GRAPH.NEIGHBORS for Moon point lookups, not Cypher. Detail: docs/reviews/2026-06-16/4FEATURE-BENCH.md.
11.5 2026-06-17 v3-2 inline-filter VERIFIED (GCloud, build 8238515)¶
The v3-2-graph-correctness milestone added the inline Filter op so MATCH (a:N {id:N}) narrows on the
node-id property instead of full-scanning the label (the §11.4 cypher_match_rows=14991 defect). This run
proves it in-harness — the 2026-06-16 harness filtered on the internal GRAPH.ADDNODE handle (a false
0); the corrected harness (docs/reviews/2026-06-17/gce-4feature-bench.sh) filters on the id property value.
| metric | §11.4 (pre-v3-2) | 2026-06-17 verified | FalkorDB |
|---|---|---|---|
Moon cypher_match_rows (x86 & ARM) |
14,991 (full scan) | 4 | 4 |
| Moon cypher_1hop qps (x86 / ARM) | 40† / 28† | 1,195 (6.68 ms) / 787 (10.15 ms) | 4,734 / 3,869 |
| Moon cypher_2hop qps (x86 / ARM) | 13† / 9† | 1,156 (6.92 ms) / 755 (10.33 ms) | 4,395 / 3,659 |
| Moon build ops/s (x86 / ARM) | 25,333 / 12,592 | 25,094 / 13,939 | 959 / 604 |
| Moon native_neighbors qps (x86 / ARM) | 5,671 / 4,474 | 5,650 / 4,442 | — |
cypher_match_rows = 4 on both arches — identical to FalkorDB's filtered 4 (was a 14,991-edge full scan).
Moon's Cypher point-query is now correct and ~28–30× faster than its old full-scan self (2-hop ~84–89×).
It still trails FalkorDB ~4× because FalkorDB has a property index on id while Moon does a filtered
label scan — a property index for inline-equality is a legitimate future optimization, explicitly out of
v3-2 scope. Moon native builds 23–26× faster and native 1-hop edges out FalkorDB Cypher.
Detail: docs/reviews/2026-06-17/4FEATURE-VERIFIED.md.
11.6 2026-07-07 Graph wave-2 criterion microbenchmarks (OrbStack Linux VM, aarch64)¶
Wave-2 engine (feat/graph-engine-wave2, PR #237): frozen-tier copy-up writes, IndexScan
ranges, row-BFS multi-segment gate, write-side plan cache, Cypher aggregations,
OPTIONAL MATCH/WITH. -C target-cpu=native, fat-LTO bench profile, criterion.
VM cores are shared — treat absolutes as indicative, relatives as solid.
| Benchmark | Median | Notes |
|---|---|---|
| 1-hop neighbor, CSR (frozen) | 1.07 ns | vs memgraph (mutable) 113.8 ns — frozen tier ~106× |
| 2-hop BFS, CSR 1K / 10K | 1.01 / 1.10 µs | vs memgraph 3.47 / 3.64 µs (~3.3×) |
| Edge insert (memgraph) | 206 ns | |
| GRAPH.ADDNODE / ADDEDGE dispatch | 223 / 204 ns | ~4.5–4.9M ops/s per-shard ceiling |
| GRAPH.NEIGHBORS dispatch | 429 ns | |
| CSR freeze, 64K edges | 19.5 ms | per-dirty-graph cost of the checkpoint graph snapshot (P0 fix) |
| Cosine similarity 384d / 768d | 31 / 57 ns SIMD | vs scalar 247 / 522 ns (~8–9×, NEON) |
| Row-BFS frozen 1-segment, 10K depth-3 | 658 µs | vs sequential memgraph BFS 9.44 ms (~14×) |
| Row-BFS frozen 2-segment | 1.65 ms | W2-6 multi-segment gate ≈ 2.5× single-segment |
| Reader BFS mixed-tier / frozen-2seg | 1.83 / 3.40 ms | |
ParallelBfs memgraph 10K depth-3 |
11.8 ms | slower than sequential 9.44 ms — see note |
ParallelBfs note: the memgraph parallel path is a structural net loss and has zero
production callers (the query path uses BoundedBfs; both share the row-BFS frozen fast
path, which is where parallelism actually pays). Its neighbor collection stays sequential
(SegmentMergeReader borrows !Send MemGraph), so it parallelizes only visited-set
filtering while paying per-level neighbor-list clones, a full FxHashSet→DashSet visited
rebuild, and raw thread::scope spawns per 128-node morsel (~76 spawns/level at a 9.7K
frontier). Candidate cleanup: retire it or fold into BoundedBfs.
11.7 2026-07-07 wave-2 vs FalkorDB (OrbStack Linux VM, Docker FalkorDB)¶
scripts/bench-graph-compare.sh --nodes 5000 (5K nodes, 3K edges), wave-2 engine.
⚠ Sequential redis-cli harness (one process per command) — per-op cost is dominated by
the ~1 ms redis-cli fork on BOTH sides, so this measures single-client latency deltas,
not server throughput; the §11.5 8-thread persistent-connection GCloud run remains the
throughput reference. Ratios:
| Operation | Moon | FalkorDB | Ratio |
|---|---|---|---|
| Node insert | 880/s | 716/s | 1.2× |
| Edge insert | 996/s | 580/s | 1.7× |
| 1-hop query | 938/s | 684/s | 1.3× |
| 2-hop query | 1,000/s | 724/s | 1.3× |
| Cypher pattern match | 862/s | 757/s | 1.1× |
Moon leads every row in this harness — including Cypher, where §11.5 (pre-wave-2, concurrent harness) trailed ~4×. The two harnesses are not directly comparable (fork-bound single client compresses server-side deltas); a fresh 8-thread GCloud run is the right follow-up before claiming the Cypher gap is closed.
11.7b 2026-07-07 Cypher-2× wave (P1 mutable property index, P2 result cache, P3 text predicates)¶
Follow-up to §11.7/§11.8: profiling showed 82.5% of shard CPU in index_scan_keys's
mutable-tier linear scan. Three features landed (commits 8d5794b9, 9b38f56c+1abc4998,
9d634607):
- P1 — mutable-tier property index: point queries stop scanning every memgraph node.
Quiet-host measurement (OrbStack, 8-thread, 5K nodes): 25,506 → 29,014 qps, p50
0.30 → 0.22 ms (−27%), shard CPU −~70% (
index_scan_keysGONE from perf profile; top symbol drops to 3.9% kernel TCP). Same-box FalkorDB ratio 2.44× → 2.78×. The win scales with graph size: the bench scans only 5K nodes; at 1M the old path is ~200× more per-query while the index probe stays O(1). - P2 — Cypher result cache (write-gen invalidated, pre-encoded RESP bytes, TinyLFU doorkeeper): interleaved A/B vs P1-only (noisy shared host, alternating runs): cache-hostile cycling −2.4% (was −21% before the doorkeeper — admission on second sighting removed the serialize+insert+O(n)-evict miss cost), hot-key repeat +1.9%. Real payoff is expensive reads (a 24 ms full-graph aggregation becomes a byte-copy), not client-bound point queries.
- P3 — text predicates via FTS reuse:
CONTAINS/STARTS WITH/ENDS WITH(new syntax) +=~now prune frozen segments through aSegmentTextIndex(presence bitmap — deliberately NOT token-postings:CONTAINS 'rust'must match"trusted", so tokenized pruning would be unsound; residual Filter stays authoritative). BM25 scoring path implemented + tested, Cypher surface syntax deferred.
11.8 2026-07-07 wave-2 vs FalkorDB — 8-thread GCloud harness: Cypher gap CLOSED¶
Same harness as §11.5 (graph phase of gce-4feature-bench.sh: 5K nodes, 15K edges,
redis-py, 8 threads, 6s per op class), wave-2 engine @ 1bbaec61, same-box FalkorDB
(Docker). ⚠ Instance is e2-standard-16 (2.2 GHz shared-core Xeon; c2d capacity
exhausted in-zone), much weaker than §11.5's machines — cross-run absolutes are NOT
comparable, but the same-box Moon:FalkorDB ratio is the valid metric.
| metric | Moon (wave-2) | FalkorDB | ratio | §11.5 ratio (pre-wave-2) |
|---|---|---|---|---|
| cypher_1hop qps (p50) | 3,489 (1.77 ms) | 3,879 (2.02 ms) | 0.90× | 0.25× |
| cypher_2hop qps (p50) | 4,518 (0.89 ms) | 3,701 (2.11 ms) | 1.22× | 0.26× |
| build ops/s | 10,321 | 535 | 19× | 26× |
| cypher_match_rows | 5 | 5 | correct parity | 4 = 4 |
| native_neighbors qps | 3,185 | — | — | — |
The June deficit — FalkorDB's property index vs Moon's filtered label scan — is gone at this scale: Moon Cypher point queries improved ~3–4× relative to FalkorDB on the same box (wave-2 write-side plan cache + IndexScan + executor work). Moon now WINS 2-hop at better p50 and ties 1-hop within 10% on the weakest instance class; on equal §11.5-class hardware the 1-hop tie likely flips too (confirmed in §11.9 — dedicated cores flip 1-hop to a 2.3–2.7× Moon win). Caveat: Moon's p99 (18.9 ms 1-hop) trails FalkorDB's (3.1 ms) on this shared-core instance — §11.9 shows this was a shared-core artifact (dedicated-core p99 beats FalkorDB). Interesting inversion: Moon Cypher 1-hop (3,489 qps) now beats its own native GRAPH.NEIGHBORS (3,185 qps) under concurrency — the plan cache amortizes parsing to near-zero.
11.9 2026-07-07 Cypher-2× wave vs FalkorDB — GCE dedicated-core validation (x86 + ARM)¶
Supersedes §11.8's shared-core e2 caveat. Same 8-thread 1-hop Cypher point-query workload
(5K nodes / 15K edges, seed(7), 20 s measure, redis-py persistent connections), run on
dedicated GCE instances — c3-standard-8 (Xeon Platinum 8481C) and t2a-standard-8
(Neoverse-N1) — with Docker FalkorDB on the same box, 2 interleaved reps each. Fresh
native build of branch HEAD 852d53a (post P1 property index / P2 result cache / P3).
| metric | x86 Moon | x86 FalkorDB | ratio | ARM Moon | ARM FalkorDB | ratio |
|---|---|---|---|---|---|---|
| cypher_1hop qps (rep1 / rep2) | 11,641 / 11,620 | 4,988 / 4,955 | 2.34× | 10,938 / 10,867 | 4,028 / 4,135 | 2.67× |
| p50 (ms) | 0.54 | 1.46 | 2.7× | 0.61 | 1.81 | 2.9× |
| p90 (ms) | 1.36 | 2.30 | — | 1.33 | 2.83 | — |
| p99 (ms) | 2.48 | 3.33 | — | 2.26 | 4.31 | — |
Moon wins 1-hop Cypher outright on both architectures — 2.3–2.7× the QPS at ~2.7–2.9× better p50 — and rep-to-rep spread is <0.2% (dedicated cores). The §11.8 p99-tail concern (18.9 ms on the shared-core e2) is confirmed as a shared-core artifact: on dedicated cores Moon's p99 beats FalkorDB's on both arches. Ratios also validate the same-box VM readings (§11.7b 2.78×, final-HEAD 2.55×). FalkorDB build rate for context: 1,009 ops/s (x86) / 605 ops/s (ARM) via batched UNWIND — Moon's build side used the sequential single-client native API in this harness, so build is not compared here (see §11.5 for the 23–26× concurrent-build comparison).
12. Full-Text Search¶
Date: 2026-06-16
Engine: inverted index in src/text/ — BM25 (k1=1.2, b=0.75), FST term dictionary, RoaringBitmap postings,
analyzer pipeline (NFKD → lowercase → UAX#29 → stopword → Snowball English stem); TAG + NUMERIC stores.
Surface: RediSearch-compatible FT.CREATE (TEXT/TAG/NUMERIC) · FT.SEARCH · FT.AGGREGATE.
12.1 vs RediSearch (GCloud, 100K docs, 8 concurrent clients)¶
100K docs, Zipf vocabulary (8000 terms), TEXT + TAG + NUMERIC schema; vs redis/redis-stack-server. x86 shown
(ARM mirrors within ~15%). qps (p50):
| query | Moon | RediSearch | note |
|---|---|---|---|
| index docs/s | 376 | 18,052 | Moon ~48× slower to index |
| term high-DF (~5k docs) | 19 (419 ms) | 3,813 (2.1 ms) | Moon O(M²) TF-lookup cliff |
| term mid-DF (~1k docs) | 429 (18.6 ms) | 4,909 (1.0 ms) | Moon slower |
| term low-DF (~95 docs) | 7,765 (0.81 ms) | 3,606 (1.9 ms) | Moon faster (small posting, lock-free) |
| AND (2-term) | 7,665 | 8,485 | parity |
OR (\|, 2-term) |
7,686 | 3,474 | ⚠ Moon doesn't union (total 10 vs 2072) |
TAG @cat:{} |
7,791 | 3,319 | ⚠ Moon reports returned-count not total (10 vs 5064) |
NUMERIC @price:[..] |
107 (74.7 ms) | 351 (22.8 ms) | Moon ~3× slower |
| TEXT + TAG combo | 12,843 | 4,077 | ⚠ Moon returns 0 hits (vs 253) |
| FT.AGGREGATE groupby | 6 (698 ms) | 11 (361 ms) | Moon ~2× slower |
12.2 Status¶
Moon's full-text search is functional but early-stage relative to RediSearch's mature engine. It is faster on
low-cardinality single-term queries (small posting lists, per-shard lock-free path) but trails on:
- Indexing throughput (~40–48× slower) — synchronous analyzer + an O(V) per-doc upsert scan (posting.rs:139).
- High-DF queries — an O(N) term-frequency lookup (store.rs:504) degrades to O(M²); a term in ~5% of docs
takes 419 ms (this was predicted by the code review and reproduced exactly here).
- Query correctness — OR (|) does not union, TEXT+TAG combos return 0, and FT.SEARCH reports the returned
count rather than the total-matched count.
These are well-scoped fixes (RoaringBitmap::rank for TF lookup, OR-union, combo-query, count semantics, async
indexing). See docs/reviews/2026-06-16/review-fts.md + 4FEATURE-BENCH.md.
12.3 2026-06-17 v3-1 hardening VERIFIED (GCloud, build 8238515)¶
The v3-1-fts-hardening milestone (PRs #190/#192) targeted exactly the §12.2 gaps. This run confirms every
one is fixed on both arches — most importantly, every Moon hits count now equals RediSearch's exactly
(the §12.1 OR/TAG/combo counts were broken: 10/10/0). x86 shown; ARM mirrors within ~15%.
| query | §12.1 Moon | 2026-06-17 Moon | RediSearch | hits (Moon = RS) |
fix |
|---|---|---|---|---|---|
| index docs/s | 376 | 15,052 | 18,232 | — | ~40× (O(V) upsert) |
| term high-DF (~5k) | 19 (419 ms) | 240 (33 ms) | 3,791 | 5068 = 5068 | O(M²) cliff gone |
| term mid-DF (~1k) | 429 | 1,416 | 5,293 | 1046 = 1046 | — |
| term low-DF (~95) | 7,765 | 7,714 | 3,594 | 95 = 95 | Moon still wins |
OR (\|) |
7,686 (hits=10) | 688 | 3,778 | 2072 = 2072 | union fixed |
TAG @cat:{} |
7,791 (hits=10) | 2,227 | 3,367 | 5064 = 5064 | count fixed |
NUMERIC @price:[..] |
107 | 97 | 377 | 10119 = 10119 | correct |
| TEXT + TAG combo | 12,843 (hits=0) | 270 | 4,314 | 253 = 253 | combo fixed |
The §12.1 OR/TAG/combo QPS looked fast only because they did no real work (10 or 0 hits — broken). Now that
they compute the full correct result, the QPS reflects real work; Moon still trails RediSearch on raw
multi-term QPS but indexes at near-RediSearch rates and is correct, and wins low-DF single-term. All
four §12.2 gaps — OR union, TAG/NUMERIC total-match counts, TEXT+TAG combo, O(V) index + O(M²) high-DF — are
resolved. Detail: docs/reviews/2026-06-17/4FEATURE-VERIFIED.md.
13. Data Correctness¶
13.1 Consistency Test Suite¶
scripts/test-consistency.sh runs 132 tests comparing moon output against Redis as ground truth.
| Category | Tests | Status |
|---|---|---|
| String SET/GET (empty, 1B, 12B SSO, 13B heap, 64B-64KB, numeric, float) | 14 | PASS |
| String mutations (APPEND, INCR/DECR, STRLEN, GETRANGE, SETRANGE, GETDEL, GETSET) | 16 | PASS |
| APPEND crossing SSO->heap boundary (11B -> 13B) | 1 | PASS |
| MSET / MGET (with missing keys) | 2 | PASS |
| SET options (EX, PX, NX, XX, SETEX, SETNX, TTL verify) | 8 | PASS |
| Binary-safe data (null bytes, tabs, newlines, UTF-8) | 3 | PASS |
| Hash operations (HSET/HGET/HGETALL/HMGET/HDEL/HINCRBY + large values) | 9 | PASS |
| List operations (RPUSH/LPUSH/LRANGE/LLEN/LINDEX/RPOP/LPOP + large values) | 8 | PASS |
| Set operations (SADD/SCARD/SISMEMBER/SREM/SMEMBERS) | 5 | PASS |
| Sorted Set operations (ZADD/ZCARD/ZSCORE/ZRANK/ZRANGE/ZINCRBY) | 8 | PASS |
| Bulk load (1K deterministic keys, 50 random spot-checks) | 51 | PASS |
| Overwrite / type change (size changes, string->hash) | 5 | PASS |
| Edge cases (nonexistent, DEL+GET, SET NX/GET, 500-char key) | 5 | PASS |
| Total | 132 | ALL PASS |
Tested across all shard configurations:
| Shards | Result |
|---|---|
| 1 | 132/132 PASS |
| 4 | 132/132 PASS |
| 12 (auto) | 132/132 PASS |
13.2 Known Unimplemented Commands¶
GETRANGE/SETRANGE— not yet implemented (returnsERR unknown command)
14. Architecture Notes¶
14.1 Data Structure Sizes¶
| Struct | Size | Notes |
|---|---|---|
| CompactKey | 24 B | Inline keys <= 22 bytes (zero heap alloc) |
| CompactEntry | 24 B | CompactValue(16) + ttl_delta(4) + metadata(4) |
| CompactValue | 16 B | SSO <= 12 bytes inline; heap strings use HeapString |
| HeapString | 24 B | Vec<u8> — no enum discriminant, no Bytes/Arc overhead |
| DashTable Segment | ~3 KB | 64B ctrl + 8B meta + 60 slots x (24B key + 24B value), align(64) |
| Segment load threshold | 90% | 54/60 slots; avg fill ~67% |
14.2 Key Optimizations Applied¶
| Optimization | Impact | Component |
|---|---|---|
| HeapString(Vec |
~35% less per heap string | CompactValue |
| SSO (Small String Optimization) | Zero alloc for values <= 12B | CompactValue |
| CompactKey inline | Zero alloc for keys <= 22B | CompactKey |
| DashTable SIMD probing | 16-way parallel key match | Segment |
| Lock-free oneshot | Eliminated 12% CPU (mutex) | Cross-shard dispatch |
| CachedClock | Eliminated 4% CPU (syscall) | Per-shard event loop |
| Lazy Lua VM | -18MB baseline (init on first connection) | Shard startup |
| Lazy replication backlog | -12MB baseline (init on first replica) | Shard startup |
| SPSC buffer 256 entries | -6MB baseline (was 4096) | Channel mesh |
| 90% load threshold | ~8% better fill factor | DashTable |
| Per-shard WAL | Scales linearly with shards | Persistence |
| io_uring batch I/O | Amortizes syscalls | Network |
15. How to Reproduce¶
Build¶
Cargo.toml profile: lto = "fat", codegen-units = 1, opt-level = 3, strip = true
Memory & CPU Benchmark¶
# Full matrix (12 data points x 4 value sizes, ~3 minutes)
./scripts/bench-resources.sh --shards 1
# Quick mode (2 key counts x 2 value sizes, ~1 minute)
./scripts/bench-resources.sh --shards 1 --quick
# Multi-shard (real-world throughput)
./scripts/bench-resources.sh --shards 0
# Output: BENCHMARK-RESOURCES.md
Data Consistency¶
./scripts/test-consistency.sh --shards 1 # 132 tests, ~25 seconds
./scripts/test-consistency.sh --shards 4 # Cross-shard dispatch
./scripts/test-consistency.sh --shards 0 # Full auto (12 shards)
Throughput Benchmark¶
# Quick comparison
redis-benchmark -p 6400 -c 50 -n 100000 -t SET,GET -P 16 -q
# Production scenarios (10 workloads)
./scripts/bench-production.sh --shards 1
# Multi-shard scaling
./scripts/bench-production.sh --shards 4
./scripts/bench-production.sh --shards 8
With Persistence¶
# Start with AOF
./target/release/moon --port 6400 --shards 1 --appendonly yes --appendfsync everysec &
redis-server --port 6399 --save "" --appendonly yes --appendfsync everysec --daemonize yes
# Benchmark writes
redis-benchmark -p PORT -c 50 -n 200000 -t SET,INCR,LPUSH,HSET -P 16 -q
Durability write-path A/B (§7.3)¶
Same-instance, alternated-rep harness that produced the §7.3 matrix. Run inside a Linux VM (fsync numbers are meaningless on shared-core / non-pinned macOS).
# Fresh server + dir per scenario, 3 reps, engine order flips each rep.
# For EACH (durability, workload) pair, compare Moon --shards 2 vs Redis stock:
for dur in nodur:'--appendonly no' \
esec:'--appendonly yes --appendfsync everysec' \
always:'--appendonly yes --appendfsync always'; do
moon_args=${dur#*:}
MOON_DISK_FREE_MIN_PCT=0 ./target/release/moon --port 6399 --shards 2 --dir "$(mktemp -d)" $moon_args &
redis-server --port 6399 --dir "$(mktemp -d)" --save '' $moon_args & # matching policy
# p1 (no pipeline) AND p16:
redis-benchmark -p 6399 -t set -c 8 -n 300000 -r 100000 -d 64 # p1
redis-benchmark -p 6399 -t set -c 8 -n 300000 -r 100000 -d 64 -P 16 # p16
done
# Provenance probe — confirm the code path is actually engaged:
# park-free writer (#241): strace -c the shard-N thread; futex calls should be
# ~10s (esec), NOT ~150k. (parked recv = the old path; a regression.)
# batch-coalesced write (#242): strace -c aof-writer-N under always -P 16;
# write(2) count should be << record count (one write per batch, not per record).
sudo strace -c -p "$(ps -T -p <moon_pid> -o tid,comm | awk '/shard-0/{print $1}')"
Pub/sub fan-out delivery A/B (§7.3, PR #240)¶
# N subscribers on one channel + one pipelined publisher; metric = messages
# DELIVERED per second across all subscribers (wire-frame count on each socket).
# Drops are allowed by the slow-subscriber policy, so delivered-throughput IS
# the ceiling under test. Harness: scratchpad pubsub_delivery_bench.py.
./target/release/moon --port 6399 --shards 2 --appendonly no &
python3 pubsub_delivery_bench.py 6399 8 5 # 8 subs, 5s
# Coalesced build delivers ~5M msg/s at ratio 1.000; a per-message-write build
# drops ~100% under flood (subscriber queue cap = 256).
GCloud Linux Benchmark¶
# Provision instances
gcloud compute instances create moon-bench-x86 \
--zone=us-central1-a --machine-type=c3-standard-8 \
--image-family=ubuntu-2404-lts --image-project=ubuntu-os-cloud \
--boot-disk-size=50GB --boot-disk-type=pd-ssd
gcloud compute instances create moon-bench-arm64 \
--zone=us-central1-f --machine-type=t2a-standard-8 \
--image-family=ubuntu-2404-lts-arm64 --image-project=ubuntu-os-cloud \
--boot-disk-size=50GB --boot-disk-type=pd-ssd
# Setup (on each instance)
bash scripts/gcloud-bench-setup.sh
# Run full benchmark suite (KV + vector + graph)
bash scripts/run-full-bench.sh
# 4-feature concurrent-vs-competitor pass (KV/Vector/Graph/FTS vs Redis/RediSearch/FalkorDB) — §2.9/§10.6/§11.5/§12.3
# Use the 2026-06-17 harness: graph filters on the `id` PROPERTY value (not the GRAPH.ADDNODE handle), builds MOON_BRANCH (default main).
# Pulls redis-stack + falkordb Docker images; env-tunable dataset sizes.
scp docs/reviews/2026-06-17/gce-4feature-bench.sh <instance>:~/ && \
ssh <instance> 'MOON_BRANCH=main VEC_NUM=50000 FTS_NDOC=100000 GRAPH_NN=5000 GRAPH_NE=15000 BENCH_DUR=8 bash ~/gce-4feature-bench.sh'
# Cleanup
gcloud compute instances delete moon-bench-x86 --zone=us-central1-a --quiet
gcloud compute instances delete moon-bench-arm64 --zone=us-central1-f --quiet
Notes¶
- Co-located benchmarks (client + server on same machine) are conservative. Separate-machine benchmarks with 25+ GbE show higher throughput.
- GCloud VM results vary 10-15% between runs (noisy-neighbor CPU sharing). Always compare Moon/Redis ratios from the same run, not absolute RPS across runs.
- macOS RSS is a high-water mark. Use fresh server instances per data point for accurate memory measurement.
- Always use
redis-benchmark -r <num_keys>to generate unique keys. - redis-benchmark 8.x uses
\rfor progress lines. Pipe throughtr '\r' '\n'before parsing. - Moon production defaults include disk-offload (WAL v3 + PageCache). For raw throughput comparison, explicitly pass
--appendonly no --disk-offload disable.