Benchmarking Methodology
How VeltrixDB's benchmark numbers are produced: the harness, the hardware, the pass/fail gates, and the raw results with the conditions they were measured under — so you can reproduce them or run the same harness against your own workload shape.
The harness
scripts/bench.sh runs a 6-phase sequenced workload (build + start, MPUT bulk load, cool-down, read-only, mixed 70R/30W, sustained MPUT write stress) and reports go/no-go on two hard gates: packing density and GC emergency runs. The project re-runs it after any storage engine change.
It builds the server and loadtest with CGO_ENABLED=0 (Go map index) unless you export CGO_ENABLED=1. The Docker image is a cgo build (native index), so set CGO_ENABLED=1 on Linux to measure what the image runs.
# macOS / Linux dev (uses /tmp, 1M keys)
./scripts/bench.sh
# Linux production-class (n2-highmem-64, 8 NVMe disks)
DATA_DIRS=/mnt/nvme0,...,/mnt/nvme7 \
RAW_VLOGS=/dev/nvme0n1,...,/dev/nvme7n1 \
CACHE_MB=409600 NUM_KEYS=10000000 \
CONCURRENCY=512 BULK_DUR=120 STRESS_DUR=300 \
./scripts/bench.sh
Key environment variables
| Var | Default | Notes |
|---|---|---|
VALUE_SIZE | 128 | Match your real value distribution |
NUM_KEYS | 1,000,000 | Use 10M for production-scale runs |
BATCH_SIZE | 1024 | Higher = better packing density |
CONCURRENCY | 64 | Use 512 on multi-core hosts |
BULK_DUR | 30 s | 120–300 s for production verification |
READ_DUR / MIXED_DUR | 30 s | Read-only and mixed phases |
STRESS_DUR | 60 s | 600 s to detect GC pressure |
WAL_WINDOW_MS | 5 | Sets both --wal-flush-window-ms and --vlog-flush-window-ms (the server default is 15) |
CACHE_MB | 1024 | 409600 on n2-highmem-64 |
DATA_DIRS | /tmp/veltrixbench | Comma-separated list → --data-dirs; only paths under /tmp are wiped first |
RAW_VLOGS | — | Raw block-device VLog (Linux, root + CAP_SYS_RAWIO) |
OUT_DIR | /tmp/veltrixbench-out | Binaries, server.log, run.log |
EXTRA_SERVER_FLAGS | — | Extra server flags, e.g. --disable-ordered-index |
CGO_ENABLED | 0 | Build mode of the server and loadtest |
Pass criteria
The harness enforces two hard gates. A benchmark run that fails either gate is not representative of the storage engine operating correctly.
| Gate | Condition | What failure means |
|---|---|---|
| Density | bytes/record ≤ 1.2 × (24 + value_size) (veltrixdb_vlog_file_bytes / Δveltrixdb_storage_writes_total after the bulk load) | Packing not engaged — keep BATCH_SIZE > 1 (loadtest --batch-size) |
| GC emergency | veltrixdb_vlog_gc_emergency_runs_total delta == 0 during the write stress | Write rate exceeds GC throughput |
The script exits non-zero if either gate fails. It also prints the read P99, but that is not a gate.
Reference numbers (BENCHMARKING.md)
write(2) WAL flusher, one pwrite per VLog batch, the binary WAL, the MultiPut cache write-around, the ordered-index overwrite skip and the native index, all of which change write-path numbers. The macOS figures also sit on an fsync that does not flush the drive cache. Re-run before quoting them. The single-Put rows are fixed-window group commit; the default is now adaptive (see durable single-key writes).macOS M-series (dev)
| Metric | Value |
|---|---|
| MPut throughput (1024 batch) | ~360K writes/s |
| Bytes/record at 128 B | ~160 B (25× density) |
| Single-Put P99 (5 ms window) | ~8 ms |
Linux n2-highmem-64 (8 NVMe) — projected
| Metric | Value |
|---|---|
| MPut throughput (1024 batch) | ~3M writes/s |
| Single-Put P99 (5 ms window) | ~5.2 ms |
| GET P99 cache-hit | ~52 µs |
| 1B keys × 128B disk total | ~149 GB (vs 4 TB unpacked) |
YCSB benchmark results (100M keys)
These are the only reproducible headline numbers. Published in BENCHMARK_RESULTS.md, dated June 9, 2026, on a historical pure-Go build (v1.0; it predates the native index, the binary WAL, the one-write(2) WAL flusher, the ordered-index fix and --net). Test environment: AWS EC2 high-memory instance, 495 GB total RAM (488 GB available), 4× NVMe SSD at 873 GB each (3.5 TB total), raw block-device VLog (/dev/nvmeXn1p2), 400 GB LIRS cache, 200 client threads, 100,000,000 keys, ~100-byte values, benchmark tool YCSB 0.17.0. The YCSB binding used for this run is not in the repository, so it cannot be re-run from it; bench/compare (go-ycsb, below) is the reproducible YCSB path.
Write benchmark (100M key load)
Configuration: 5 ms WAL flush window, 5 ms VLog flush window (the server default is now 15 ms), 200 threads, 4× NVMe striped, every write fdatasync-durable.
| Metric | Value |
|---|---|
| Total keys written | 100,000,000 |
| Throughput | 18,064 ops/sec |
| Total runtime | 92.3 minutes |
| Avg write latency | 11.06 ms |
| Min latency | 413 µs |
| P95 latency | 30.8 ms |
| P99 latency | 47.3 ms |
| Failures | 0 (100% success) |
| Durability | WAL + fdatasync on every write |
Read benchmark (10M reads on 100M key dataset)
Configuration: 400 GB cache, 200 threads, 10,000,000 operations, 100% reads (YCSB workload_c). The entire dataset was in cache, so every read was a cache hit — this measures the cache-hit path through the network stack, not NVMe reads.
| Metric | Value |
|---|---|
| Throughput | 427,697 ops/sec |
| Total runtime | 23.4 seconds for 10M reads |
| Avg read latency | 461 µs |
| Min latency | 10 µs |
| P95 latency | 2,069 µs |
| P99 latency | 2,771 µs |
| Max latency | 53.6 ms |
| Hit rate | 100% (0 NOT_FOUND) |
Progression across runs
BENCHMARK_RESULTS.md tracks how throughput and hit rate changed as the dataset and tuning scaled up:
| Run | Write speed | Read speed | Read hit rate |
|---|---|---|---|
| 1M keys · 50 threads · 10ms WAL | 2,036 ops/sec | 4,000 ops/sec | 48.5% |
| 10M keys · 50 threads · 10ms WAL | 3,164 ops/sec | 301,386 ops/sec | 79% |
| 100M keys · 200 threads · 5ms WAL | 18,064 ops/sec | 427,697 ops/sec | 100% |
Later measurements New in v1.5–v1.7
Not YCSB and not the YCSB machine; each row states its conditions. These cover the native index and binary WAL (v1.6) and the ordered-index fix and C++ front-end (v1.7).
| Measurement | Result | Conditions |
|---|---|---|
Server batch writes, 8 clients × 1024-key MPUT, 1M-key space | 3.54M keys/s, P99 4.2 ms (1.45M, 9.6 ms before the ordered-index fix) | macOS (fsync does not flush the drive cache) |
| Batch writes, Go front-end, C++ storage off | 1.77M keys/s, P99 12.2–12.7 ms | 4-CPU CI runner, tmpfs, client on same host, 2026-09-25 |
| Reads, same configuration | 177–182K ops/s, P99 1.8–2.0 ms | same |
| Mixed-workload write P99, 1 ms flush window | 2.57 ms | same |
| Batch writes, C++ storage layer all on (io_uring bridge + SQPOLL, batch engine, native index) | 1.12M keys/s | same; per-part attribution pending |
C++ network front-end (--net=cpp, opt-in) vs Go | read P50 154 vs 286 µs, poll read throughput 194K vs 177K ops/s; read P99 ~3.0 vs 1.8 ms, batch writes ~12% lower | same, measured before disk reads moved off the loop; not faster overall |
| Native index vs Go map, 5M keys | full GC 21 → 0.27 ms, settled RSS 168 → 142 B/key, ~19 ns more per cache-miss lookup | engine benchmark; go test ./storage -run '^$' -bench 'Index_Footprint|Index_Get' -benchtime 1x compares the two (footprint uses 2M keys by default) |
| In-process cache hit | ~92 ns (sharded LIRS; was 711 ns) | engine benchmark |
Because the CI rows use tmpfs and a client on the same host, compare them with each other, not with other hardware. Reproduce them with scripts/net-bench.sh or the “Net front-end” workflow: ENGINES=nocgo NETS=go WINDOWS="5 1" for the “C++ storage off” rows, ENGINES=sqpoll for the “all on” row, NETS="go uring poll" for the front-end row. The macOS 3.54M row is the same batch phase (loadtest --mode write --concurrency 8 --batch-size 1024 --num-keys 1000000) against a local server.
Network front-ends and storage configurations (net-bench.sh) New in v1.7
scripts/net-bench.sh starts a fresh server per combination and runs batch write (8 × 1024-key MPUT, BATCH_DURATION, default 10 s), read (64 binary GET clients) and mixed (70/30) workloads, then prints one markdown table (also to $GITHUB_STEP_SUMMARY). The “Net front-end” GitHub workflow runs it on Linux, where the io_uring code exists.
# Front-ends (NET:N = N event loops for a C++ front-end)
NETS="go uring poll uring:2" ENGINES=native WINDOWS=5 ./scripts/net-bench.sh
# Storage configurations under the Go front-end
ENGINES="native bridge sqpoll map nocgo" NETS=go WINDOWS="5 1" ./scripts/net-bench.sh
Defaults: NETS="go uring", ENGINES=native, WINDOWS=5, DURATION=15 s per read/mixed workload, NET_THREADS = CPU count. The C++ front-ends need the binary protocol, so reads and mixed run with loadtest --proto=binary.
ENGINES value | Configuration |
|---|---|
native | Native C++ index, io_uring VLog bridge off — the default build |
inline | native, but the C++ front-end reads disk on its loop thread (VELTRIXDB_NET_DEFER_READS=0) |
bridge / sqpoll | native + VELTRIXDB_URING_BRIDGE=on / sqpoll |
map | Go map index, bridge off |
nocgo | VELTRIXDB_DISABLE_CGO_ENGINE=1: map index, no C++ storage layer |
- Every overwrite appends to the WAL and VLog and nothing reclaims space during a run; one combination leaves ~5–10 GB, so each data dir is deleted when its combination finishes.
- Each workload runs under a watchdog (duration +
WATCHDOG_SLACK, default 60 s). On a hang the script dumps native and Go stacks, records a FAILED row, and moves on. - C++ front-ends log per-stage latency at shutdown (log2 buckets, so
p99<=8msmeans [4, 8) ms). A highcb_entermeans loop threads are waiting for a Go P; try fewer loops (uring:2). - On macOS only poll with one loop runs, and loopback caps round trips at ~60K/s for both front-ends — measure on Linux.
October 2026 measurements New in v1.11–v1.12
Taken while building adaptive group commit, vector / text / hybrid search, PQ and the disk graph. All on one macOS laptop (Apple silicon) unless stated, so absolute numbers do not predict a Linux NVMe server, and none of them is a comparison with another database. Each table names the test or tool that reproduces it. Source: BENCHMARK_RESULTS.md, “October 2026 measurements”.
Durable single-key writes: adaptive vs fixed group commit
--group-commit=adaptive (the default since v1.12) vs fixed. The device’s fdatasync cost is emulated (a sleep before the real sync), because macOS fsync returns at the drive cache; that is what makes the result mean the same on a laptop as on Linux. 15 ms window, 128 B values, each writer doing sequential durable Puts for 1.5 s.
VELTRIX_GC_TABLE=1 go test ./storage -run TestGroupCommit_LatencyTable -v
Emulated sync = 300 µs (NVMe-like):
| Writers | Mode | P50 | P99 | Writes/s | Writes per fdatasync |
|---|---|---|---|---|---|
| 1 | fixed | 16.01 ms | 17.76 ms | 63 | 1.0 |
| 1 | adaptive | 0.45 ms | 0.52 ms | 2,186 | 1.0 |
| 8 | fixed | 16.02 ms | 16.39 ms | 501 | 8.0 |
| 8 | adaptive | 0.96 ms | 1.22 ms | 8,283 | 8.0 |
| 64 | fixed | 15.94 ms | 16.81 ms | 4,053 | 64.0 |
| 64 | adaptive | 1.67 ms | 2.64 ms | 37,504 | 63.9 |
| 256 | fixed | 16.40 ms | 33.15 ms | 15,291 | 249.3 |
| 256 | adaptive | 3.18 ms | 11.54 ms | 73,999 | 255.2 |
Emulated sync = 2 ms (slow / network disk):
| Writers | Mode | P50 | P99 | Writes/s |
|---|---|---|---|---|
| 1 | fixed | 18.10 ms | 23.68 ms | 55 |
| 1 | adaptive | 2.41 ms | 2.52 ms | 417 |
| 64 | fixed | 18.18 ms | 19.12 ms | 3,541 |
| 64 | adaptive | 5.40 ms | 7.07 ms | 11,691 |
| 256 | fixed | 18.32 ms | 35.17 ms | 13,705 |
| 256 | adaptive | 6.96 ms | 18.56 ms | 32,277 |
TestGroupCommit_AdaptiveGates (every PR) fails if a lone writer waits for the window or concurrent writers stop sharing syncs. The nightly workflow prints the table in its job summary.
YCSB A–F through the server (go-ycsb, VeltrixDB only)
bench/compare/cmd/ycsb (go-ycsb core workloads) against a local server: 20,000 records × 10 fields × 100 B, 20,000 operations, 16 threads, zipfian, whole-record writes. Client and server on the same laptop; macOS fsync does not reach the platter, so write latency here is a lower bound. Not comparable with the June YCSB 0.17.0 run.
cd bench/compare && START=0 DBS=veltrixdb RECORDS=20000 OPS=20000 THREADS=16 WORKLOADS="a b c d e f" ./compare.sh
| Workload | Op | Ops/s | Avg | P99 | P99.9 |
|---|---|---|---|---|---|
| load | INSERT | 37,495 | 419 µs | 1.29 ms | 2.04 ms |
| A (50/50 read/update) | total | 34,640 | 453 µs | 1.30 ms | 2.98 ms |
| READ | 17,230 | 262 µs | 0.77 ms | 1.55 ms | |
| UPDATE | 17,434 | 642 µs | 1.63 ms | 3.14 ms | |
| B (95/5) | total | 52,255 | 303 µs | 0.65 ms | 0.74 ms |
| C (read only) | READ | 55,287 | 287 µs | 0.39 ms | 0.53 ms |
| D (read latest) | total | 55,163 | 288 µs | 0.39 ms | 0.59 ms |
| E (short scans) | SCAN | 23,651 | 638 µs | 1.43 ms | 1.97 ms |
| F (read-modify-write) | total | 40,067 | 392 µs | 0.81 ms | 0.98 ms |
| READ_MODIFY_WRITE | 13,397 | 915 µs | 1.22 ms | 1.49 ms |
compare.sh defaults: DBS="veltrixdb aerospike scylla", WORKLOADS="a b c f", RECORDS=1000000, OPS=1000000, THREADS=64, START=1 (docker compose up / stop each database in turn). With START=0 it uses databases already running at VELTRIX_ADDR (default 127.0.0.1:9000), AEROSPIKE_HOST and SCYLLA_HOSTS. bench/compare is a separate Go module (Go ≥ 1.25).
Vector search
Real embeddings. First 100,000 words of GloVe-100 (angular), 100 dimensions, 500 queries, exact cosine ground truth. In-process, one query at a time, macOS (Apple silicon, GOMAXPROCS = 18). Build = 18 parallel durable PutVector writers.
scripts/ann-dataset.py glove-100-angular /tmp/ann --train 100000 --test 500
VELTRIX_ANN_DIR=/tmp/ann go test ./storage -run TestRealEmbeddings -v -timeout 30m
| Layout | Build | ef | recall@10 | p50 | p99 | QPS (1 thread) |
|---|---|---|---|---|---|---|
| float32 | 57 s | 64 | 0.842 | 0.25 ms | 0.41 ms | 3,933 |
| 128 | 0.906 | 0.42 ms | 0.63 ms | 2,351 | ||
| 256 | 0.953 | 0.77 ms | 1.12 ms | 1,307 | ||
| 512 | 0.983 | 1.43 ms | 1.95 ms | 712 | ||
| int8 | 50 s | 64 | 0.839 | 0.61 ms | 0.81 ms | 1,655 |
| 128 | 0.905 | 0.55 ms | 0.80 ms | 1,905 | ||
| 256 | 0.952 | 0.72 ms | 1.08 ms | 1,356 | ||
| 512 | 0.983 | 1.18 ms | 1.84 ms | 834 | ||
| pq (m = 25) | 42 s | 64 | 0.797 | 0.56 ms | 0.90 ms | 1,771 |
| 128 | 0.870 | 0.72 ms | 1.06 ms | 1,367 | ||
| 256 | 0.927 | 1.17 ms | 2.91 ms | 780 | ||
| 512 | 0.969 | 1.56 ms | 3.44 ms | 604 | ||
| pq + disk graph | 45 s | 64 | 0.788 | 0.55 ms | 0.79 ms | 1,811 |
| 128 | 0.875 | 0.66 ms | 0.99 ms | 1,506 | ||
| 256 | 0.931 | 1.06 ms | 2.18 ms | 907 | ||
| 512 | 0.966 | 1.60 ms | 2.97 ms | 613 |
The quantized layouts are slower per query than float32 at this size because they re-rank from the VLog (one parallel MultiGet; before that change int8 at ef = 64 was p50 1.9 ms / p99 13.8 ms). Their point is RAM: at 100 dimensions the vectors are small and graph edges dominate. scripts/ann-dataset.py needs h5py and numpy.
Over the network (vecbench). GloVe-100, first 20,000 words, 300 queries, int8, 8 concurrent clients, server and client on one macOS machine. Load: 6.4 s (3,146 durable vector writes/s).
cd bench/compare && go run ./cmd/vecbench -data /tmp/ann -train 20000 -quant int8 -threads 8
# flags: -addr (default 127.0.0.1:9000) -efs (default 64,128,256,512) -queries -pqm -graph memory|disk -out results/
| ef | recall@10 | QPS | p50 | p99 |
|---|---|---|---|---|
| 64 | 0.894 | 24,637 | 0.28 ms | 0.68 ms |
| 256 | 0.985 | 15,629 | 0.48 ms | 0.91 ms |
RAM per vector, 768 dimensions. 10,000 synthetic clustered vectors, Go heap per vector, macOS.
VELTRIX_VECTOR_MEMORY=1 go test ./storage -run TestVectorMemoryTable -v
| Layout | Heap / vector | Mapped file / vector | vs float32 |
|---|---|---|---|
| float32 | 3,383 B | — | 1× |
| int8 | 1,078 B | — | 3.1× less |
| pq, m = 96 | 493 B | — | 6.9× less |
| pq, m = 96 + disk graph | 378 B | 216 B | 8.9× less heap |
| Other vector measurement | Result | Reproduce |
|---|---|---|
| CI quality gate, 5K × 64-dim clustered, recall@10 | f32 0.927, ef 256 0.998, int8 0.923, pq 0.882, pq + disk 0.881, filtered 0.994, after 40% deletes ~0.96 | every PR (node-7-search): go test ./storage -run TestSearchQualityGate -v |
| HNSW rework, 20K × 128 random, index only | search 240 → 99 µs (853 → 6 allocs); with 50% deleted 6.56 ms → 190 µs; insert 349 → 209 µs | go test ./storage -run '^$' -bench 'HNSW_(Search|Search_Deleted50pct|Insert)$' |
Tested up to 100K real vectors and 10K at 768 dimensions; nothing at 10M+ yet.
Search soak and crash chaos
| Test | Result (macOS) | Reproduce |
|---|---|---|
| Soak, 60 s: 4 writers + 4 searchers, 5K-id space, upserts / deletes / searches against an oracle of acknowledged writes | 201,573 writes, 292,079 searches, 0 results outside the oracle, 288,025 self-queries with 0 misses, RSS flat at ~1.0 GB, exact oracle match before and after restart | VELTRIX_SOAK_DURATION=60s go test ./tests/integration -run TestSearchSoak -v |
| Crash chaos: SIGKILL mid-write, restart, rebuild | 3 cycles, 8,524 / 8,380 / 11,851 acknowledged writes, all visible after restart + rebuild, no deleted id returned | VELTRIX_CHAOS_CYCLES=3 go test ./tests/integration -run TestSearchCrashRecovery -v |
| Raft failover read barrier | without it a new leader missed the acknowledged write in 20 / 20 test rounds; with it 0 / 20 | go test ./consensus -run TestWaitLeaderApplied_FailoverVisibility -v |
The nightly workflow runs a 20-minute soak and 10 crash cycles on Linux:
VELTRIX_SOAK_DURATION=20m go test ./tests/integration/ -run TestSearchSoak -v -timeout 40m
VELTRIX_CHAOS_CYCLES=10 go test ./tests/integration/ -run TestSearchCrashRecovery -v -timeout 25m
Writes in flight at the kill are not checked. After each crash cycle the test rewrites every id whose write had an unknown outcome to a fresh, fully acknowledged version, so later cycles check those ids too.
Consistency and rolling-upgrade tests New on main after v1.16 — ships in the next release
Jepsen-style harness (tests/jepsen, Go standard library only, all in _test.go files): 3-node raft and replicated clusters of real server processes, 8 clients on the text protocol (GET, PUT, CAS, INCR, set adds), and a nemesis that SIGKILLs or SIGSTOPs the leader or a random node, crashes a follower mid-InstallSnapshot, and — through an iptables hook on Linux as root — partitions it. Checkers: per-key linearizability (Wing & Gong search with memoisation, shrunk to a minimal failing history), counters, sets, read validity and copy counts. A negative control must find stale reads.
| Test | Configuration | Checked |
|---|---|---|
TestJepsenRaftLinearizable | raft, --linearizable-reads | Every key linearizable including reads; counters; sets |
TestJepsenRaftLocalReads | raft, local reads on random nodes | Writes, CAS, INCR and final leader reads linearizable; ordinary reads for validity only (follower reads may be stale); all nodes converge |
TestJepsenReplicated/strong, /quorum | replicated, kill / pause only | Only what the mode promises: an acknowledged write is on RF (strong) or RF/2+1 (quorum) nodes once healed; no value no client wrote. No linearizability check |
TestJepsenNegativeControl_StaleReads | raft, local reads in the linearizability check | Must find a violation |
go test ./tests/jepsen/ -short -timeout 120s # checker unit tests only
go test ./tests/jepsen/ -v -timeout 15m # every mode, 20 s of workload each
VELTRIX_JEPSEN_DURATION=5m VELTRIX_JEPSEN_SEED=42 \
go test ./tests/jepsen/ -v -run TestJepsenRaftLinearizable -timeout 20m
Limits: one host (shared disk, kernel and clock; faults are process-level), no real partitions on macOS, no clock skew, power loss not simulated, single-key text-protocol operations only (no multi-key TXN, MPUT, namespaces, search or binary protocol).
Rolling-upgrade tests (tests/upgrade, VELTRIX_UPGRADE_TEST=1): real v1.16 and current-build processes on the same data dirs — raft rolling upgrades with and without snapshots, mixed-version snapshot transfers in both directions, a replicated rolling upgrade, standalone upgrade and downgrade (clean stop and SIGKILL), and raft downgrade with and without a wipe. They found the v1.16 snapshot TTL loss on upgrade (fixed) and corrected the downgrade guidance.
VELTRIX_UPGRADE_TEST=1 go test ./tests/upgrade/ -v -timeout 30m # builds v1.16 from the tag
Both run nightly (.github/workflows/nightly.yml, jobs jepsen — with and without --raft-pipeline, partitions enabled — and upgrade).
Against Aerospike and ScyllaDB (same hardware)
docker-compose.yml and aerospike.conf (and, since v1.15, docker-compose.cluster.yml and the 3-node Aerospike confs) are compile-checked only. There is no vector driver for Aerospike Vector Search or ScyllaDB vector search.bench/compare/compare.sh runs go-ycsb workloads through a VeltrixDB driver and go-ycsb’s Aerospike and Cassandra (ScyllaDB) drivers, one database at a time on the same machine, and writes results/<ts>/summary.md.
# On the machine under test (Linux, NVMe), with Docker and Go ≥ 1.25:
export BENCH_DATA=/mnt/nvme0/bench BENCH_CPUS=8 BENCH_MEM=16g
./compare.sh # all three, workloads a b c f
RECORDS=10000000 OPS=10000000 THREADS=128 WORKLOADS="a b c d f" ./compare.sh
# Vectors (VeltrixDB only):
../../scripts/ann-dataset.py glove-100-angular /mnt/nvme0/ann --train 1000000 --test 1000
VECTORS=1 ANN_DIR=/mnt/nvme0/ann VEC_ARGS="-quant pq -graph disk" ./compare.sh
Publish a result only with all of these stated next to it: the same machine and disk with one database running at a time (a client on a second machine is better still); the durability setting of each (VeltrixDB ACKs after fdatasync of WAL and VLog; ScyllaDB’s default commitlog_sync: periodic ACKs before the write is on disk, so use batch or report both; Aerospike ACKs from its write buffer); warm-up and a dataset larger than the caches if the point is disk performance; the replication factor (1 with NODES=1, 3 with NODES=3); and the versions of all three.
3-node clusters, replication factor 3 Fixed in v1.15
NODES=3 sets RF 3 for every database and needs three addresses per database. For VeltrixDB: VELTRIX_ADDRS (comma-separated), VELTRIX_MODE=raft|replicated, and VELTRIX_RAFT_PIPELINE=true|false so the summary records which --raft-pipeline setting the servers ran. The VeltrixDB driver homes threads round-robin over the addresses and reads locally; writes follow the first MOVED <leader> and share the leader address across threads; MOVED - (election running) waits and retries; a connection error fails over to the next listed node. After the run, cmd/keycount checks that every node holds every record. The guarantees differ per database (raft: quorum commit through the leader; ScyllaDB: QUORUM; Aerospike CE: all three copies), so state them next to any number.
# servers already running on three hosts
START=0 NODES=3 VELTRIX_MODE=raft \
VELTRIX_ADDRS=10.0.0.11:9000,10.0.0.12:9000,10.0.0.13:9000 DBS=veltrixdb ./compare.sh
# GCP: 3 server VMs with local NVMe + 1 client, raft-off / raft-on / replicated, then deletes everything
PROJECT=<your project> ./gcp-multihost.sh
START=1 NODES=3 runs three containers per database on one machine (docker-compose.cluster.yml) to check that clusters form and drivers route — not for performance. That compose file is syntax-checked only; the 3-node VeltrixDB path was run as local processes on one laptop, against raft and replicated clusters and with a raft leader killed mid-run. Laptop figures, which are not cluster results (three raft nodes share one drive): the incremental raft log took load from 70 to 1,046 inserts/s (one run each), and --raft-pipeline was slower on load (560 vs 1,038 inserts/s, medians of 2 runs). No multi-host run has been published yet.
Internal GKE run (unverified)
Reported conditions: 3-node GKE cluster · n2-highmem-64 · 8×375 GB NVMe per node · 1 billion keys · 80R/20W · 30 minutes sustained.
| Metric | Value |
|---|---|
| Reads/s (internal GKE run — unverified) | 7,200,000 |
| Writes/s (internal GKE run — unverified) | 1,800,000 |
| P50 (80R/20W blended) | ~220 ns |
| P99 (80R/20W blended) | ~510 µs |
| GC emergency events | 0 |
| Errors | 0 |
| Storage density | ~160 GB for 1B × 128 B values (by arithmetic from the bench.sh density of ~160 B per 128 B record, not a billion-key measurement) |
Verified vs unverified: the measured, reproducible numbers are the YCSB single-node results (427,697 reads/s · 18,064 durable writes/s · AWS EC2, 4×NVMe, 100M keys). The 3-node GKE figures (7.2M reads/s / 1.8M writes/s) come from an internal run without a published harness — treat them as unverified until reproduced.
Single node, historical — mixed provenance
Pure-Go path with the fixed group-commit window (before adaptive group commit became the default in v1.12). Each row says whether it was measured, computed or projected; none is a current benchmark run.
| Operation | P50 | P99 |
|---|---|---|
| GET — cache hit | 0.05 ms | 0.28 ms (~1.4M reads/s, measured on macOS) |
| PUT — 15 ms fixed flush window | 5 ms | 15.2 ms (Linux NVMe) |
| PUT — 5 ms window, 512 workers | 2.6 ms | 5.2 ms (~102K writes/s is the computed ceiling, not a measurement) |
| MultiPut 1024 entries | — | ~9.5 ms (~426K entries/s is a Linux projection from macOS ~108K/s) |
VeltrixDB vs. Redis, head to head
The VeltrixDB column is from the YCSB run above. The Redis column was not measured — it is a range of commonly cited figures, included for orientation only.
| Mode | VeltrixDB | Redis |
|---|---|---|
| Durable writes (fsync every write) | 18,064 ops/sec | 10,000–30,000 ops/sec |
| Non-durable writes (no fsync) | N/A — always durable | 100,000–200,000 ops/sec |
VeltrixDB always writes durably. Redis requires appendfsync always for an equivalent guarantee, which reduces Redis to 10K–30K ops/sec — comparable to or slower than VeltrixDB at that durability level.
| Resource | VeltrixDB (100M keys) | Redis (100M keys) |
|---|---|---|
| RAM required | 15 GB as recorded (index; 400 GB provisioned as cache). Today’s per-key figures give ~14–23 GB (~142 B/key native index + ~90 B/key ordered index) | 30–45 GB (data lives in RAM; estimate, not measured) |
| Disk required | ~25 GB per NVMe | Swap/RDB snapshots only |
| Max dataset size | 3.5 TB (NVMe) | Limited by RAM |
| Dataset > RAM | Works (reads from NVMe) | Evicts or crashes |
Key takeaways
- Writes: 18,064 durable writes/sec across 4 NVMe disks at a 5 ms window, zero failures across 100M operations.
- Reads: 427,697 reads/sec at 461 µs average latency, with the whole dataset in the 400 GB cache (100% hits). Cache-miss (NVMe) read performance was not measured in this run.
- Disk footprint: 100M keys used ~25 GB per disk out of 873 GB available. Larger key counts (1 billion+) have not been measured reproducibly; note the index itself stays in RAM (~142 B/key with the native index).
- Durability: every write is crash-safe (WAL +
fdatasync). Redis needsappendfsync alwaysfor a comparable guarantee. - Read hit rate: 100% with proper dataset sizing (
recordcountmatchingoperationcount), zeroNOT_FOUNDacross 10 million reads.
scripts/net-bench.sh and engine benchmarks (September 2026, v1.7 code); the October 2026 tables from the Go tests and bench/compare commands printed above each (v1.11–v1.12 code, macOS laptop); the reference tables from scripts/bench.sh and projections. Re-run the harness against your own key/value shape rather than assuming these numbers transfer directly — see VeltrixDB vs. Redis for how to interpret the results relative to your workload.