Benchmark: both servers vs mongod¶
Generated 2026-08-26 on a dedicated DigitalOcean instance (8 vCPU, x86-64
Linux), against mongod 8.0.31, via invoke do-perf.
All three servers use the same WiredTiger storage engine — mongod ships
it; SecantusDB vendors the same C library — driven by the same pymongo
client over the wire protocol. The hot path differs only above the storage
layer (command dispatch, query planner, operator engines), so this is a fair
comparison of the parts of SecantusDB that aren’t WiredTiger itself.
Each workload runs against a freshly-spawned server on a free port with its
own tmp data dir, all on-disk WiredTiger. Each timed 5× per server; the table
reports the median in milliseconds and how many times slower than mongod
each server is. Dataset is 10,000 small docs.
Note
These numbers are not comparable to those published before 2026-08-22. Two things changed at once, and both move the ratios without any change to SecantusDB:
The reference moved from mongod 6.0.16 to the 8.0 line (8.0.31 as of this run; the harness installs the latest 8.0.x). Every
×mongodfigure is a ratio, so a faster denominator makes us look worse. mongod 8.0 is substantially quicker at inserts, and that alone accounts for most of the change — SecantusDB’s own absolute timings barely moved.The machine moved from a developer laptop to a dedicated droplet. A laptop cannot be trusted for this: a background build or an OS indexer shifts every column at once and nothing in the output says so. One earlier run recorded mongod itself at 2.5× its own baseline, which would have published a regression that did not exist.
The results file now records the mongod version it measured against, so a future change of reference can’t be mistaken for a change in SecantusDB.
Results¶
Workload |
mongod |
Rust server |
×mongod |
Python server |
×mongod |
|---|---|---|---|---|---|
insert (10k docs) |
73.2 ms |
148.4 ms |
2.0× |
807.7 ms |
11.0× |
find indexed range |
10.0 ms |
9.1 ms |
0.9× |
68.0 ms |
6.8× |
find full scan |
17.8 ms |
17.1 ms |
1.0× |
126.5 ms |
7.1× |
find filtered scan |
15.6 ms |
16.2 ms |
1.0× |
165.1 ms |
10.6× |
update_many (half) |
96.6 ms |
120.2 ms |
1.2× |
1601.8 ms |
16.6× |
aggregate $group |
11.1 ms |
20.6 ms |
1.9× |
258.6 ms |
23.4× |
aggregate multi-stage |
15.6 ms |
37.5 ms |
2.4× |
268.9 ms |
17.3× |
delete_many (half) |
47.3 ms |
75.2 ms |
1.6× |
788.0 ms |
16.7× |
change-stream drain |
104.0 ms |
111.0 ms |
1.1× |
207.5 ms |
2.0× |
* Change-stream drain: 5,000 events consumed through a watch() cursor
(only the drain is timed). mongod’s number is measured against a throwaway
single-node replica set — its change streams require one — while every
other row keeps the standalone-mongod reference, so the rest of the table
stays comparable with earlier publications.
Reading the numbers¶
The Rust server runs at ~0.9×–2.4× of mongod per operation, and two rows beat it: indexed range at 0.9× and full scan at 1.0×. Filtered scan (1.0×) and the change-stream drain (1.1×) sit at parity;
update_manyis 1.2×. The widest gaps stay on the aggregation paths ($group1.9×, multi-stage 2.4×) andinsert(2.0×), which is dispatch and operator work above a storage engine that is literally the same C library.The read rows improved sharply in 0.6.0b11:
getMorehad been reusing mongod’s 101-document first-batch default on every batch, so a 10,000-document scan paid ~100 round trips where mongod pays 2. Removing that round-trip tax took the full scan from ~2.2× to parity.The Python server runs at ~2.0×–23.4× of mongod on these workloads — the low end is the change-stream drain, where the work is oplog reads rather than per-document compute — and the Rust server is correspondingly ~1.9×–13.3× faster than the Python server workload-for-workload. The largest gaps are the update-heavy and aggregation paths, where Python does the most per-document work.
Every number includes the wire protocol and
pymongodriver overhead a real client pays — these are end-to-end times, not engine microbenchmarks.The numbers are single-machine, single-process, no concurrency — a deliberately narrow scenario to isolate per-operation latency. Throughput under concurrent connections is a separate measurement (and a place where mongod’s connection pooling / async accept loop wins regardless).
The trade is unchanged: conformance and WiredTiger durability over raw per-op latency. For ephemeral test and dev data the wall-clock difference rarely matters; when it does, that’s what the Rust server is for. See The two servers and the feature comparison for what each supports.
How to refresh¶
# The embedded Rust server needs the storage-engine build:
SKBUILD_CMAKE_DEFINE=SECANTUS_BUILD_STORAGE_ENGINE=ON uv sync --extra dev
uv run --no-sync python -m bench.compare_servers --n 10000 --reps 5
Requires mongod on PATH (Community Server is enough; --no-mongod skips
it and compares the two SecantusDB servers only). On macOS:
brew tap mongodb/brew && brew install mongodb-community.
Over a real network, against a real MongoDB¶
The numbers above are single-host: server and client share one machine, so
the “network” is loopback and the load generator competes with the database
for the same cores. The harness in
bench/DO_CLUSTER.md
measures the deployment shape instead — one server droplet, two separate
client droplets, real NICs between them — and runs SecantusDB and a real
mongod back-to-back on the same hardware, interleaved across passes so
drift lands on both equally.
Measured 2026-08-26 on DigitalOcean lon1: a c-4 (4 vCPU, 8192 MB) server and 2 x c-2, 16 workers each, 8 KiB incompressible
documents, a 70/20/10 insert/find/update mix, a 4G WiredTiger cache for
both engines, and 3 interleaved passes:
engine |
version |
ops/s (median) |
spread |
p50 |
p99 |
p99.9 |
server CPU |
|---|---|---|---|---|---|---|---|
SecantusDB |
0.5.3-beta.163 |
9,338 |
3.1% |
2.48 ms |
16.76 ms |
37.34 ms |
78.9% |
mongod |
8.0.31 |
12,698 |
2.6% |
1.92 ms |
12.48 ms |
31.62 ms |
78.0% |
SecantusDB reaches 0.74x of MongoDB’s throughput on this workload, with p50 latency within 1.29x and p99.9 within 1.18x. Both engines saturated the same server while the clients sat idle, so both figures are server-bound and the comparison is fair. Run-to-run spread was about 3.1%.
Tail latency is where this has moved most. Two releases ago the p99.9 ratio was
2.0x; it is now 1.18x. Before the block compressor changed it was
72x — profiling found 65% of server CPU inside zlib’s deflate, and
switching the default to lz4 cut p99.9 from 1,303 ms to 37 ms in one step. What
remains is a real throughput gap, no longer dominated by any single cause.
Caveats, in both directions:
The payload matters. These documents are incompressible. On compressible documents both engines do better and the ratio shifts, because compression ratio starts paying for itself. Real workloads sit somewhere between.
This is one workload shape. Write-heavy, small documents, single-node, no secondary indexes beyond
_idand the benchmark’s own. It is a useful comparison, not a general claim.Tail latency is still the weaker axis. p50 is close; p99.9 is 2x. If your workload is write-heavy and latency-sensitive at the tail, measure with your own data before switching.
Reproduce with invoke do-bench --repeat 3 --payload random (needs a
DigitalOcean API token; the harness provisions, measures and destroys).