Performance Framework
Run deterministic fast and deep real-world rsql workloads.
The benchmark runner in client/perf starts an isolated rsql process, seeds
deterministic domain data, executes scenario workloads, samples server metrics,
and writes a bottleneck report.
Run profiles
cd client
bun run perf:fast
bun run perf:deep
bun run perf:report --run=<run-id-or-path>Use a short smoke run while changing the framework:
bun run perf:fast --quick=truefast is the normal local feedback profile. deep increases namespace counts,
rows, repetitions, load phases, and optional profiling.
Published 1,000-namespace results
The published run provisions 1,000 databases with 100 rows each. Four load-generator processes issue 256 concurrent requests while rotating across the complete fleet. Every repetition executes a fixed operation count and then checks the exact persisted row count.
The reported value is the median of three repetitions. The run was repeated after all lifecycle and cache changes, and every repetition verified persisted row counts.
| Workload | Throughput | p50 | p95 | p99 |
|---|---|---|---|---|
| Point reads | 54,051 requests/s | 3.75 ms | 12.24 ms | 17.98 ms |
| Batched inserts | 5,711 requests/s | 29.43 ms | 152.76 ms | 283.05 ms |
Each insert request contains 20 rows, producing 114,226 written rows/s with the
default synchronous=full durability. All requests succeeded and every
repetition persisted the expected number of rows.
The read workload used 225 MB peak RSS and retained about 4,000 open files when the benchmark deliberately kept all 1,000 namespace handles warm. Before read/write pools were initialized independently, the same workload used 343 MB and about 6,000 open files. Production defaults to 128 retained handles. A deployment may provision far more namespaces than it keeps simultaneously hot.
Cold fleet startup
A separate startup check populated control.db with 100,000 active namespace
records while leaving all tenant databases cold. After a clean checkpoint,
rsql became healthy in 53.65 ms with 22.4 MB RSS and 19 open files. No tenant
database was opened and all 100,000 registry rows remained present.
This check validates the startup architecture rather than namespace request capacity: startup opens the control and telemetry stores but does not enumerate registry rows or scan namespace files.
Read-pool profiles
The default read-pool limit of one connection is optimized for broad multi-tenant traffic. Raising the limit to four favors many concurrent reads against one hot database. The pool itself still opens only on the first read.
| Traffic shape | Read connections | Throughput | p95 |
|---|---|---|---|
| 1,000 rotating namespaces | 1 | 54,051 requests/s | 12.24 ms |
| 1,000 rotating namespaces | 4 | 25,006 requests/s | 34.84 ms |
| One hot namespace | 1 | 27,614 requests/s | 26.62 ms |
| One hot namespace | 4 | 57,812 requests/s | 12.27 ms |
Configure this with --namespace-read-connections or
RSQL_NAMESPACE_READ_CONNECTIONS. Benchmark the actual tenant distribution
before changing it.
Durability profile
synchronous=full remains the default. Hosts that accept losing the newest WAL
transactions after an operating-system crash or power loss can opt individual
namespaces into synchronous=normal.
| Mode | Written rows/s | p95 | p99 |
|---|---|---|---|
full |
121,071 | 149.38 ms | 277.36 ms |
normal |
188,619 | 96.23 ms | 128.75 ms |
The two modes use the same binary, data, client load, and fixed-operation
protocol. normal improved row throughput by 56% in this run. It is a
durability tradeoff, not a free default.
Process sharding
The same 1,000 namespaces were distributed across independent rsql processes on one host while total client concurrency remained fixed.
| Processes | Point reads/s | Read p95 | FULL rows/s | Write p95 |
|---|---|---|---|---|
| 1 | 23,881 | 34.63 ms | 134,902 | 115.94 ms |
| 2 | 38,275 | 26.76 ms | 102,524 | 159.46 ms |
| 4 | 42,635 | 22.00 ms | 93,062 | 247.70 ms |
Read throughput scales across process boundaries. FULL-durable writes do not scale when every process shares one physical volume; additional writers increase fsync and disk contention. Use separate volumes or hosts when sharding write-heavy tenants.
Scenario coverage
The matrix covers namespace lifecycle, schema changes, point reads, filters/pagination/search, concurrent writes, bulk/upsert, read-only SQL, SSE fanout, import/export, overview/changelog polling, noisy-neighbor isolation, and cold/warm behavior.
Each scenario validates data correctness in addition to latency and throughput.
Artifacts
A run records:
- host, toolchain, commit, dirty state, configuration, and seed
- operation-level latency/status events
- one-second Prometheus and process samples
- scenario p50/p95/p99, throughput, errors, and variation
- database and WAL growth
- prioritized bottleneck hypotheses
- optional CPU, heap, mutex, and block profiles
Benchmark results are machine-specific. Compare runs on the same host with the same profile and seed rather than treating one number as a universal capacity claim.
The complete scenario and report schema remains documented in
client/perf/README.md in the repository.