Backend Benchmarks

August 7, 2026 ยท View on GitHub

The benchmark reports single-process baseline performance separately from four-process contention/scaling. Every phase in a profile uses the profile's same number of distinct spawned processes. Results are local medians, not universal performance claims. Higher throughput and lower point-read latency are better.

Global comparison: 2026-08-07

Each profile used three isolated repetitions of the same 1,000 JSON-shaped documents and deterministic query order. In the four-process profile, the original doc-0 through doc-999 IDs were partitioned by the sharded SQLite hash into batches of 243, 263, 241, and 253. Four synchronized processes submitted those batches through insert_many(); raw SQLite used executemany(). Before signaling readiness, every worker opened the database and collection and forced lazy backend initialization. Process launch, connection setup, schema/catalog checks, and phase warm-up were outside the timer.

The four-process read-all phase had each process decode the complete collection and reports 4,000 returned documents divided by the slowest worker duration. The 200 warmed exact-ID targets were divided into deterministic worker streams and report both aggregate throughput and per-operation latency. The 100 update IDs and 100 delete IDs were partitioned into disjoint streams of individually acknowledged exact-ID operations, so every target changed exactly once and every sharded mutation remained shard-affine. Each phase completed and joined all workers before the next phase began.

Every available row records the requested distinct PID count for all five phases in every repetition. Backend order rotated between repetitions. Persistent stores were closed, reopened, and fully validated before final size was measured.

Environment: current TinyMongo working tree, Python 3.9.6, SQLite 3.51.0, macOS 26.2 arm64, and an Apple M1 Pro. Experimental SQLite used four shards.

The table intentionally combines the available one-process baselines with the four-process concurrency results. The process count is explicit on every row.

BackendProcessesInsert workloadInsert docs/sRead-all docs/sPoint reads/sPoint avg msPoint p95 msUpdate docs/sDelete docs/sFinal KiB
TinyMongo Memory4not runnot runnot runnot runnot runnot runnot runnot runN/A
TinyMongo TinyDB11 spawned insert_many bulk89,877141,5313003.3373.51838066.4
TinyMongo TinyDB4failed: AlreadyLocked during updatenot publishednot publishednot publishednot publishednot publishednot publishednot publishedN/A
TinyMongo Parquet11 spawned insert_many bulk3,33777,9701636.1246.9543368.9
TinyMongo Parquet44 spawned insert_many bulks954311,5565456.2506.6293469.0
TinyMongo SQLite11 spawned insert_many bulk64,566130,0812,2150.4510.611260246296.0
TinyMongo SQLite44 spawned insert_many bulks1,296320,2524,1200.8161.317117119300.0
TinyMongo SQLite-sharded (4)44 spawned shard-affine insert_many bulks50,573425,27254,1770.0620.1003,0663,979544.0
TinyMongo DuckDB4not runnot runnot runnot runnot runnot runnot runnot runN/A
Raw SQLite (native SQL)44 spawned executemany bulks263,6842,329,023237,8830.0140.02115,64820,122136.0
MongoDB4not runnot runnot runnot runnot runnot runnot runnot runN/A

TinyDB's prior four-process row has been retired. A fresh isolated run failed with AlreadyLocked during individually acknowledged updates; retrying until a run happened to pass would hide a real concurrency limitation. The harness published no partial measurements from that failed run.

The paired one- and four-process profiles show different effects by phase. Parquet's aggregate full-scan throughput scaled across CPU processes, while its individually acknowledged mutations remained constrained by whole-file rewrites. Standard SQLite also improved aggregate read throughput, but its one writer and lock acquisition dominate concurrent bulk inserts and mutation streams. Async would not remove those storage constraints.

For an unfiltered scan of up to ten shards, sharded SQLite now attaches each read-only shard to one pooled SQLite connection and executes one UNION ALL query with global natural ordering. SQLite therefore reads and merges the rows, and Python decodes each payload once. That raised the four-shard result from 206,748 to 425,272 documents/s in its paired SQLite run. In that capture, standard SQLite reached 483,199 documents/s, leaving the sharded scan about 12.0% lower rather than 50.5% lower. The separately captured stress-table row above should not replace that paired comparison. Filtered scans and databases above SQLite's default ten-attachment limit retain the established scatter/matcher path.

TinyMongo Memory is process-local and cannot expose one shared database to four spawned workers. DuckDB rejects multiple processes opening the same writable database. Both are marked not run rather than silently falling back to threads or measuring isolated databases. MongoDB was not configured for this capture; the harness uses the same five four-process phases when a URI is supplied.

Raw SQLite uses the same _id primary key and compact JSON payload, but native SQL bypasses TinyMongo's BSON handling, MongoDB-style matcher, validation, result objects, and connection lifecycle. It is a lower-bound reference rather than a feature-equivalent backend. MongoDB includes Docker loopback and server overhead.

Write durability is also not identical: TinyMongo SQLite and raw SQLite used WAL with synchronous=NORMAL, while sharded SQLite used WAL with synchronous=FULL. When configured, MongoDB uses acknowledged WriteConcern(w=1, j=True). Other rows used their normal backend defaults. N/A means the client cannot measure a meaningful persistent file size.

Run it

Single-process baseline:

.venv/bin/python tests/benchmarks/bench_storage.py \
  --backend tinydb --backend parquet --backend sqlite \
  --docs 1000 --queries 200 --repeats 3 --workers 1 \
  --json-output /tmp/tinymongo-single-process.json

Four-process concurrency profile:

TINYMONGO_MONGODB_URI='mongodb://127.0.0.1:27017/?directConnection=true' \
  .venv/bin/python tests/benchmarks/bench_storage.py \
    --docs 1000 \
    --queries 200 \
    --repeats 3 \
    --sqlite-shards 4 \
    --workers 4 \
    --json-output /tmp/tinymongo-storage.json

MongoDB is optional; without a URI its row is reported as not run. The benchmark starts no servers or persistent background workers. Each measured phase creates exactly four short-lived spawned workers and joins them before continuing. Focused regression drivers remain in tests/benchmarks/, but their different workloads are intentionally not mixed into this global table.

To refresh only one backend row, pass --backend for only that backend:

.venv/bin/python tests/benchmarks/bench_storage.py \
  --backend sqlite-sharded \
  --docs 1000 \
  --queries 200 \
  --repeats 3 \
  --sqlite-shards 4 \
  --workers 4 \
  --json-output /tmp/tinymongo-sqlite-sharded.json