Skip to main content

Performance and benchmark evidence

These are measured local results, not promises. Each number depends on the machine, payload, durability, protocol, concurrency, QoS tier, and data shape. Raw JSON artifacts live under bench/results/.

Reproduce​

Build a release server and benchmark binary, then use a dedicated QoS tier:

cargo build --locked --release -p ryme-server -p ryme-bench
rymedb-cli qos tier dedicated_cluster dedicated_cluster
ryme-bench resp --addr 127.0.0.1:6380 --workload mixed --ops 40000 --concurrency 8 --pipeline 1 --json

Measure RESP MGET end to end with a local or clustered server:

ryme-bench resp --addr 127.0.0.1:6380 --workload mget --mget-keys 256 \
--ops 10000 --concurrency 8 --pipeline 1 --json

Here ops and pipeline count commands; each command reads mget-keys values, and reported latency covers writing the request and reading its full reply. Prepopulate those keys on the target server to measure hits. In cluster mode, this includes owner routing and local/remote value handling.

A localhost smoke benchmark on an AMD Ryzen 5 4500 used Standard storage, memory durability, 16 seeded 16-byte values, and one client issuing 2,000 16-key MGET commands per capture. Three captures measured p50 at 37–49 µs, p99 at 62–72 µs, and 20.3k–25.9k commands/s, with zero protocol errors. The segment cache warmed during the run. This includes client encoding and reply parsing, but is not a cold-disk or production deployment result; see the raw captures.

An eight-client Standard-mode capture on the same host used a dedicated-cluster QoS tier, 16 seeded 16-byte values, and 16,000 16-key MGET commands per capture. Two sequential runs measured 63.6k–64.0k commands/s, p50 at 111–112 µs, and p99 at 308–328 µs, with zero errors. This remains a localhost, memory-durability smoke benchmark, not a production or cross-database comparison; see the raw captures.

The Standard-mode batch-read path resolves all segment handles under one cache lock when the active segments are resident, instead of reacquiring the cache lock for each segment. With 80 active segments, eight clients, and 16-key MGETs, two local captures measured 41.8k–45.3k commands/s and p99 at 456–501 µs, with zero errors. Treat these as a local scaling baseline, not a cross-database comparison; see the raw captures.

To isolate the in-memory transaction core from networking and WAL durability:

ryme-bench local --ops 200000 --concurrency 8 --value-bytes 128

This reports parallel commit throughput, failed commits, and a separate parallel point-read probe. By default it writes distinct keys. Set --keyspace below --ops to measure steady-state overwrites, for example --keyspace 1000; the benchmark prepopulates that keyspace before timing. Failed commits are included in the output because concurrent writes to a hot keyspace can conflict under optimistic concurrency control. Use a single worker when measuring overwrite cost without concurrent conflicts. This is a core microbenchmark, not an end-to-end database comparison.

To benchmark the SQL executor in-process without PostgreSQL-wire or network overhead:

cargo run --locked --release -j 1 -p ryme-bench -- sql-local \
--rows 1000 --ops 1000 --json

The command seeds a bounded in-memory table outside the timed interval, warms the query, then measures a filtered projection with three predicates. It checks the result row count on every operation and reports p50/p95/p99 latency and throughput. This isolates executor/storage work for one fixed query shape; it does not measure SQL parsing, concurrent clients, WAL durability, network overhead, or establish a cross-database performance claim.

Before selective JSON projection, three sequential release captures on an AMD Ryzen 5 4500 with 1,000 rows, 10,000 operations, one worker, and 42 result rows measured p50 at 84.9–86.7 µs and throughput at 11.3k–11.6k queries/s. p99 ranged from 103.6–136.6 µs, so tail latency was less stable than the median. These are local in-process baselines, not end-to-end database results; see the raw captures.

SQL projection now deserializes only selected JSON roots instead of building a complete serde_json::Value for each result row. Sequential before/after release captures on the same Ryzen 5 4500 measured median-of-capture p50 falling from 86.6 to 82.8 µs for 42 results (4.4%), and from 1.012 to 0.982 ms for 417 results (3.0%). Tail percentiles varied, and this is a single-host in-process result, not a general database speed claim; see the before/after captures.

To compare tracked single-key reads with read-only and batched reads in both storage modes:

cargo run --locked -j 1 -p ryme-bench -- txn-read-batch \
--records 10000 --candidates 1024 --iterations 100 --batch-size 256 --json
cargo run --locked -j 1 -p ryme-bench -- txn-read-batch \
--records 10000 --candidates 1024 --iterations 100 --batch-size 256 \
--standard-mode --json

Standard mode seeds a temporary segment-backed database, warms its cache, and removes the temporary directory after the run. This measures cached point-read overhead, not cold disk latency or a full networked deployment. Compare modes on the same idle machine; the output includes the storage mode and read percentiles. read_only_batched_read models the transaction-core access path used by RESP MGET: one read-only transaction with repeated get_many calls. Its speedup is reported against read_only_single_read, so neither side pays conflict-observation tracking.

--candidates may exceed --records; the benchmark then cycles through the seeded keys, measuring repeated-key batches. JSON reports the number of distinct keys per iteration. For example, use --records 64 --candidates 1024 to measure 16 reads per distinct key while keeping batch size fixed.

On an AMD Ryzen 5 4500, two release captures with 64 records, 1,024 reads per iteration, and batches of 256 measured read-only batched p50 at 157–161 µs vs 184–185 µs for individual reads in Hot mode (1.15–1.17×). Standard mode measured 323–326 µs vs 536 µs (1.65–1.66×). This is a warmed, repeated-key core workload, not end-to-end RESP or cold-disk latency; see the raw captures.

A segment-batch duplicate-elimination prototype was not retained: its unique-key read-only p50 regressed from 490 µs to 588 µs with hashing and 529 µs with sorting. Repeated-key batches benefited, but the general path must not penalize the unique-key workload without a cheaper exact duplicate check.

Standard segment point reads previously Bloom-checked, binary-searched the segment key vector, then searched the same key in the segment engine. Removing the redundant vector search reduced read-only batched p50 from 490 µs to 296–298 µs for 1,024 unique reads, and from 323–326 µs to 221–222 µs for 64 keys repeated 16 times. Single-read p50 also fell from 705 µs to 494–497 µs for the unique workload. The unique baseline is one capture; after values are two captures. False-positive Bloom results still go through the authoritative engine lookup. See raw before/after captures.

On an AMD Ryzen 5 4500, two sequential release captures with 10,000 records, 1,024 reads/iteration, 100 iterations, and batches of 256 showed read-only batch p50 of 588–592 µs versus 812–817 µs for individual read-only gets in Standard mode (1.38× in both captures). Hot mode measured 275–277 µs versus 291–295 µs (1.05–1.07×). The Hot-mode p99 varied substantially, so treat these as preliminary in-process results, not end-to-end RESP latency claims. See the read-only batch captures.

Two sequential release captures on an AMD Ryzen 5 4500 (10,000 records, 1,024 reads/iteration, 100 iterations, 256-key batches) measured Standard-mode batch p50 at 1.403–1.410 ms versus 1.645–1.646 ms for individual reads (1.17× in both captures). Hot-mode results varied between a small batch win and a small loss, so this change does not establish a reliable Hot-mode speedup. These are single-host microbenchmarks, not cross-database claims; see the raw captures.

Serializable reads record conflict keys in a BTreeSet. For sorted, unique batches whose keys do not overlap the transaction's existing read set, tracking uses the set insertion result instead of a separate membership lookup. Other batch shapes and single-key reads keep duplicate-safe membership checks. If a resource limit rejects a newly inserted key, it is removed so the transaction remains internally consistent. Two sequential Hot-mode release captures (10,000 records, 1,024 unique reads, 256-key batches, 128-byte values) measured tracked batched-read p50 at 538–539 µs, down from 861–863 µs (about 38%), and staged-write batched-read p50 at 568–569 µs, down from 865–867 µs (about 34%). Read-only batched p50 stayed around 82 µs. This is an in-process transaction benchmark, not end-to-end database latency; see the read-set captures.

To measure the in-memory vector index's build, exact-search, and HNSW-search paths with the same generated corpus:

cargo run --locked --release -j 1 -p ryme-bench -- vector-index \
--vectors 2000 --dimensions 64 --queries 200 --partitions 4 --top-k 10 --ann-ef 64 --json

The command reports dataset generation separately from index-build time, then measures exact and adaptive ANN API search latency/QPS and recall@k. Exact and ANN call order alternates per query to reduce systematic cache-order bias. The ANN API uses exact search when a partition's vector count is no larger than min(effective_ef × 2 × M, 2048); otherwise it searches the HNSW graph. M is 16 below 64 dimensions and 32 at 64 dimensions or above. The deterministic SplitMix-derived corpus avoids the duplicate vectors produced by simple periodic generators. The vector payload is capped at 16 MiB to avoid unexpectedly large allocations; index metadata and graph edges require additional memory. Use identical corpus size, dimensions, partitions, top-k, ann-ef, and build profile when comparing revisions. This isolates the Rust index implementation; it does not measure HTTP overhead, persistence, replication, or establish a cross-database performance claim. Query percentiles are descriptive only with the reported sample count (200 is too small for stable p99 conclusions).

On an AMD Ryzen 5 4500, release build, 64 dimensions, four partitions, top-k 10, ann-ef=64, and 100 deterministic queries, the adaptive ANN API measured p50 at 300.5 µs with exact recall at 4,096 vectors; partitions below the effective_ef × HNSW_M work threshold use exact search. At 8,192 vectors it used HNSW and measured 632.6 µs p50, 0.978 mean recall@10; exact search measured 907.1 µs p50. The raw captures record full results. These are local in-memory crossover observations, not stable tail-latency or end-to-end database claims. Repeat several times on the target machine before using them for capacity planning.

Exact vector search now borrows each indexed ID during scoring and copies only the selected top-k IDs. In a same-machine single-run comparison against 910d98d, 4,096-vector exact-search p50 moved from 302.2 µs to 217.0–217.1 µs across two post-change runs; 8,192-vector exact-search p50 moved from 907.1 µs to 547.8 µs. The adaptive 8,192-vector HNSW p50 measured 625.8 µs after the change versus 632.6 µs before it, with recall@10 unchanged at 0.978. These captures are not a statistically controlled benchmark; see the borrowed-ID comparison and repeat paired runs on the target workload.

Search width is an explicit latency/quality control. On the same machine, with 20,480 vectors (64 dimensions, four partitions), top-k 10, and 300 deterministic queries per run, three release runs at ef=64 measured median ANN p50 878 µs, 1,135 QPS, and 0.910 recall@10. At ef=128, median p50 was 1.527 ms, 648 QPS, and recall@10 was 0.981. Both settings built the index in about 2.7 seconds; exact-search p50 was about 3.38 ms. The API, CLI, and JS SDK keep ef=64 as their default to favor speed; set ef=128 when this measured recall/latency tradeoff better fits the workload. These are local index-only measurements; see the raw profile and benchmark your own corpus before choosing a production value.

The graph degree is also dimension-aware. A follow-up release profile on the same Ryzen 5 4500 used 3,000 vectors, top-k 10, one partition, and 100 queries. At 32 dimensions, M=16 measured 57.5 µs ANN p50, 100.2 µs exact p50, 0.983 recall@10, and 140 ms build time. At 384 dimensions, the original M=16 measured 445 µs ANN p50 and 0.645 recall@10; M=32 measured 709 µs, 0.891 recall@10, and 1.30 s build time, while exact p50 was 1.07 ms. For a larger 64-dimensional corpus (20,480 vectors, four partitions), two runs with M=32 measured 1.46 ms ANN p50, 0.988 recall@10, and 4.21–4.34 s build time. At 384 dimensions, ef=128 measured 983 µs p50 and 0.971 recall@10; ef=256 reached exact recall but was slower than exact search. These local measurements show the tradeoff that motivated using a wider graph only for embedding dimensions: better recall while ANN remained faster than exact in these samples, at the cost of index-build time and memory. They are not stable tail-latency or cross-database claims; see the dimension profile and repeat on your own corpus.

To measure full-text search when a common term matches every document:

cargo run --locked --release -j 1 -p ryme-bench -- text-index \
--documents 10000 --queries 500 --partitions 4 --top-k 10 --json

The benchmark returns p50/p95/p99 and QPS for a warmed, in-process index. On the same host and corpus, bounded top-k selection followed by direct single-term posting traversal reduced median query p50 from 1.716 ms to 98.8 µs and raised median QPS from 566 to 9,944 across three runs. The first step avoids sorting and copying every match; the single-term path also avoids building a score map. Multi-term queries retain the accumulator needed to combine term scores. See the bounded top-k runs and single-term fast-path runs. These measurements exclude network, persistence, and replication.

To profile multi-term accumulation, rerun with --query-terms 2; both common terms then match every document. Reserving the score map from the capped sum of posting lengths reduced median p50 from 1.279 ms to 696 µs and raised QPS from 745 to 1,270 in three runs. The estimate is capped at the table's document count to bound memory. See the raw profile.

To measure the sharded Hot-mode WAL write path independently of network protocols:

ryme-bench sharded-local --ops 20000 --concurrency 4 --shards 4 --value-bytes 128

This distributes writes across tables placed on each shard, reports throughput and sampled p50/p95/p99 operation latency, and removes its temporary data directory after the run. Each commit writes WAL and a delta segment. Delta segments no longer rewrite the latest pointer; snapshot writes update that pointer, and segment discovery still finds the newest delta. The default benchmark does not fsync each write; add --sync-each-write to include WAL, segment, and directory sync costs. Compare shard counts and worker counts on the same machine, and retain JSON output with hardware and durability details. This benchmark is in-process and must not be compared directly with networked database results.

Pass --standard-mode to benchmark the segment-backed Standard path with the same workload and acknowledgement policy. Output identifies the selected storage mode. Compare Hot and Standard runs sequentially on the same idle machine; they have different memory-residency semantics, so the result is a mode comparison rather than a like-for-like database claim. By default, each transaction writes one key; set --batch-size 64 to test larger commits. In that case --ops counts transactions, while qps counts individual writes and transaction_qps reports committed transactions per second. With multiple shards, rows within a batch are assigned round-robin across shard-placed tables, so larger batches exercise cross-shard coordination. Use --shards 1 to isolate the single-shard batch path; benchmark table-local batches separately when that matches the application workload.

Standard-mode scans merge the sorted key ranges of their delta segments and resolve each key from newest to oldest at the requested snapshot. They stop after collecting the requested number of visible rows instead of materializing the whole table first; scan work still depends on how many segment files must be consulted and how many tombstones or expired rows are encountered.

To measure cached Standard-mode scan pages over a snapshot plus one-row delta segments:

cargo run --locked --release -p ryme-bench -- segment-scan \
--rows 10000 --deltas 100 --limit 100 --iterations 30

Add --compact to compact the generated segment chain before warming the cache and timing scans. Compare paired runs with and without this flag to isolate the effect of compaction; compaction setup is outside the measured interval.

Setup and one cache-warming scan are outside the timed samples. The output includes p50/p95 scan latency and throughput; vary one parameter at a time and compare sequential runs on the same otherwise-idle machine.

To measure Standard-mode startup's maximum-commit discovery against the former full-segment decode path:

cargo run --locked --release -j 1 -p ryme-bench -- segment-startup \
--rows 10000 --superseded-deltas 500 --deltas 500 --iterations 10

To compare retention's former full-row decode scan with checksum-verified metadata pruning:

cargo run --locked --release -j 1 -p ryme-bench -- segment-prune \
--rows 10000 --deltas 100 --iterations 10

This creates one snapshot and a live delta tail, then alternates the previous decode-and-classify scan with SegmentStore::prune. No files are eligible for deletion in this setup, so the measurement isolates retention's segment scan and validation cost rather than filesystem cleanup. Both paths validate the segment checksum; the baseline also parses and rebuilds row indexes. The optimized path uses fixed-buffer checksum verification without rebuilding row indexes. Results use a warm filesystem cache and should be repeated on the target storage and workload.

The command creates an initial snapshot, a superseded delta run, a newer snapshot, and a live delta tail. It alternates the two measurements to reduce filesystem-cache ordering bias, and reports p50/p95 plus their p50 ratio. The full-decode path rebuilds every segment's engine, bloom filter, and sparse index; the optimized path skips files superseded by the latest snapshot and reads metadata while checksum-verifying the remaining segments with a fixed-size buffer. This isolates commit-timestamp discovery, not total database startup or cold-storage latency.

On an AMD Ryzen 5 4500 (12 logical CPUs), a release run with 10,000 snapshot rows, 500 superseded deltas, 500 live deltas, and 10 iterations measured p50 at 35.1 ms for full decode versus 4.45 ms for the optimized path (7.88×). Files were locally cached; this is a startup-work microbenchmark, not a full server restart measurement.

On the same AMD Ryzen 5 4500, the segment-prune scan with a 10,000-row snapshot, 100 delta segments, and 10 iterations measured 14.87 ms p50 for the full-decode scan versus 1.84 ms for metadata pruning (8.09×). This measures only the warm-cache retention scan with no segment deletion; it is not an end-to-end database latency result.

Two sequential A/B pairs on an AMD Ryzen 5 4500 (12 logical CPUs), comparing the pre-merge implementation at 0202d42 with the k-way merge, measured p50 latency of 7.49–7.57 ms versus 51.6–51.9 µs. The median p50 was about 145× lower; p95 was 7.59–7.81 ms versus 61.7–63.6 µs. This is a warm-cache, synthetic page workload (10,000 snapshot rows, 100 one-row deltas, limit 100), not a general database or end-to-end performance claim.

A local three-pair batch comparison on the AMD Ryzen 5 4500 used one shard, one worker, 500 transactions × 64 distinct 128-byte writes, and no per-write fsync. All runs completed with zero failures. After removing the redundant segment-engine clone, median throughput was 129.8k writes/s in Hot mode and 151.0k writes/s in Standard mode; sampled transaction p50 was 482 µs and 435 µs respectively. This is evidence for this specific mode/workload comparison, not an end-to-end database claim. The benchmark samples only eight transactions per run, so its tail percentiles are too sparse to interpret.

To isolate the delta-segment builder's owned-entry path from the borrowed API:

cargo run --locked --release -p ryme-bench -- segment-build \
--entries 1024 --value-bytes 128 --iterations 30

This compares building from a borrowed slice (which clones its entries) with building from an already-owned vector. Vector creation for the owned case is outside the timed interval because transaction code already owns that vector. It also reports p50/p95 decode time for a pre-encoded segment; encoding is not included in the decode timing. The benchmark measures construction and decode in isolation—not durable writes, transaction throughput, or end-to-end latency—and should be repeated on the same otherwise-idle machine before interpreting small differences.

On the 12-logical-CPU AMD Ryzen 5 4500 listed by the command, three release runs at 1,024 entries × 128 value bytes × 40 iterations measured borrowed-path median build times of 0.870–0.872 ms and owned-path medians of 0.762–0.766 ms. The median of the three per-run ratios was about 1.14×. Compared with the prior recorded run of this same microbenchmark, before moving the locally built engine into the segment, borrowed-path median time fell from 1.314 ms to 0.872 ms and owned-path time from 1.215 ms to 0.765 ms. This is narrowly scoped segment-construction evidence; it does not establish a transaction-throughput or end-to-end latency gain. Rerun on the target workload.

The same segment-build command also reports lookup_mean_ns for deterministic pseudo-random point hits against one already-built segment. This includes the segment Bloom/key checks, engine lookup, and returned-value clone; compare only identical entry counts, payloads, build profiles, and hardware.

Delta-segment pointer experiment​

A paired release microbenchmark used the same 12-logical-CPU AMD Ryzen 5 4500, 8 shards, 4 workers, 5,000 distinct 128-byte writes, and per-write sync. Two runs each measured 7,677 and 7,610 ops/s before, then 8,924 and 8,801 ops/s after avoiding a delta's redundant latest pointer rewrite. All runs had zero failed writes. The raw capture includes sampled latencies; p99 varied sharply between runs and is not a stable result.

This small local sample is not an SLO or a comparison with another database. Repeat on clean, identical machines before making performance claims.

For PostgreSQL-wire workloads:

ryme-bench sql --addr 127.0.0.1:5433 --user postgres --database postgres --workload mixed --ops 10000 --concurrency 8 --json

A valid run has zero errors and successful_ops == requested_ops. Record CPU, RAM, storage, server commit, durability, payload, keyspace, concurrency, and whether the endpoint is loopback or remote.

Same-machine results​

The October 2, 2026 loopback run used 12 CPUs, 15 GiB RAM, 128-byte values, 8 workers, and a warm keyspace:

WorkloadrymeDB durablep50p99Same-run Valkey
GET102,451 qps66 µs231 µsComparable read path; see raw matrix
SET67,244 qps109 µs320 µsComparable write path; see raw matrix
Mixed GET/SET87,477 qps78 µs270 µs127,475 qps, 59/124 µs in the captured mixed run
Transaction block35,390 blocks/s195 µs761 µs51,995 blocks/s, 143/399 µs

The compound transaction is intentionally stronger work: serializable OCC and durable WAL versus a non-persistent Valkey run. It was about 1.5x behind at p50 and 1.9x at p99 in this capture. The simple read path was close, not universally faster.

Replication and realtime​

The three-node loopback Raft test measured roughly 2.3 ms median leader quorum acknowledgement and 3.5 ms median follower visibility, with a maximum around 4.3 ms over 20 writes.

An earlier 100-connection, 10,000-publish realtime stress capture delivered 1,000,000 of 1,000,000 expected messages with zero receive errors. It measured approximately 266k fanout deliveries/s and 1.29k publish requests/s, but publish-to-client latency was about 5.3 s p50 and 8.6 s p99 under that burst. That historical result predates the current cached-envelope and batched-writer path; it is not directly comparable to the newer captures below.

The current release build also includes a cached broadcast envelope so one JSON payload is encoded once and reused across subscribers. A smaller 20-connection loopback run on the same 12-vCPU machine delivered all 20,000 messages with zero publish or receive errors at 13,089 publish requests/s and 2.81 million fanout deliveries/s. Publish-to-client latency was 38.9 ms p50, 69.6 ms p95, and 77.6 ms p99. This is a separate workload and should not be treated as a direct replacement for the 100-connection burst result.

Run it yourself:

ryme-bench realtime --addr 127.0.0.1:8080 --api-key local-dev-key \
--connections 100 --connection-concurrency 256 --messages 10000 \
--payload-bytes 128 --json

By default, all subscribers and publishes use the single bench channel (a hot-topic test). Set --channel-count 10 to spread both connections and publishes round-robin over bench-0 through bench-9, approximating a uniform multi-topic workload. Delivery percentage is calculated from the subscribers and successful publishes on each channel, so channels with no subscribers do not inflate the expected-delivery count.

The cached-envelope run used 20 connections, 64 connection concurrency, 1,000 messages, and 32 concurrent publishers:

ryme-bench realtime --addr 127.0.0.1:8080 --api-key local-dev-key \
--connections 20 --connection-concurrency 64 --messages 1000 \
--payload-bytes 128 --publish-concurrency 32 --json

After batching WebSocket writes (commit efb5101), two sequential runs with that same workload on the AMD Ryzen 5 4500 loopback setup delivered all 20,000 expected messages. Publish-to-client p50 was 0.572–0.669 ms and p95 was 1.408–1.792 ms, while p99 remained 109.7–111.5 ms (maximum 137–139 ms). Completed fanout throughput was 136.7k–140.1k deliveries/s. The p99 tail is still poor. These runs are a validation capture, not a controlled before/after comparison; the 100-connection/10,000-publish workload above remains the target that needs a repeat run. Raw outputs and exact invocation are in bench/results/realtime-writer-batch-20x1000.json.

The exact 100-connection/10,000-publish workload above was then run twice on the same machine against an isolated release server. Both runs delivered all 1,000,000 expected messages with no publish or receive errors. Publish-to-client p50 was 2.310–2.365 ms, p95 was 4.207–4.285 ms, and p99 was 5.418–5.572 ms; completed fanout throughput was 1.098–1.112 million deliveries/s. However, about 0.14% of samples exceeded 100 ms (maximum 157–162 ms), so the extreme tail still needs work. This is a repeated current-build validation, not a controlled before/after experiment. Raw captures are in bench/results/realtime-writer-batch-100x10000.json.

How to read the numbers​

  • Compare the same protocol and acknowledgement semantics.
  • Separate server execution, network, fsync, quorum, and client scheduling.
  • Report percentile histograms, not only average or peak throughput.
  • Never compare an in-process probe with a networked database claim.
  • Re-run on clean, identical hardware before publishing a headline.

There is no defensible blanket “10000x faster than PostgreSQL” or “100x faster than Valkey” claim. The goal is workload-specific, reproducible improvement while retaining transactions, durability, realtime, and compatibility.