Benchmark comparing SpacetimeDB 2 vs Doublets performance for basic CRUD operations with links.
SpacetimeDB is benchmarked using the official spacetimedb-sdk Rust crate connected to a running SpacetimeDB 2.0 server. Doublets is benchmarked with both in-memory (volatile) and file-backed (non-volatile) storage variants.
| Operation | Description |
|---|---|
| Create | Create a self-referential point link (id == source == target) |
| Delete | Delete links by id |
| Update | Update link source and target |
| Query All | Retrieve all links ([*, *, *]) |
| Query by Id | Retrieve a link by id |
| Query by Source | Retrieve all links with a given source |
| Query by Target | Retrieve all links with a given target |
- SpacetimeDB — connects to a running SpacetimeDB 2.0 server via the official
spacetimedb-sdkRust crate; uses thelinkstable defined in thespacetime-moduleWebAssembly module
The benchmark uses the official SpacetimeDB Rust client SDK, calling reducers to mutate data and reading from the client-side subscription cache.
- Doublets United Volatile — in-memory store; links stored as contiguous
(index, source, target)units - Doublets Split Volatile — in-memory store; separate data and index memory regions
- Doublets United NonVolatile — file-backed store; same contiguous layout but memory-mapped to a single file; data persists to disk
- Doublets Split NonVolatile — file-backed store; separate data and index files; both memory-mapped; data persists to disk
Doublets uses a recursive-less size-balanced tree for O(1) lookup by id and O(log n + k) traversal by source/target. The file-backed variants use memmap2 for memory-mapped file I/O, flushing changes to disk on drop via sync_all(). See rust/doublets-patched/PATCHES.md for why a local patched copy is used instead of the published crates.io version.
Each benchmark iteration pre-populates the database with background links to simulate a realistic database state:
- Background links:
BACKGROUND_LINK_COUNT(default: 3000) — already present before measurement - Benchmark links:
BENCHMARK_LINK_COUNT(default: 1000) — the operations being measured
The numbers below represent the amount of time (ns) a single benchmark iteration takes.
- The first chart shows time in a pixel (linear) scale. Doublets bars are drawn with a minimum visible width, otherwise they would not be visible next to SpacetimeDB.
- The second chart shows time in a logarithmic scale, to see the difference clearly, because it is around 3-5 orders of magnitude.
Charts and the table are recalculated by the
Rust Benchmark workflow on every push to main
and committed back to this repository, so the results are visible here without running
the benchmark locally.
Generated 2026-09-16 09:20 UTC by GitHub Actions run 35075414684 — 1000 benchmarked links, 3000 background links.
| Operation | Doublets United Volatile | Doublets United NonVolatile | Doublets Split Volatile | Doublets Split NonVolatile | SpacetimeDB |
|---|---|---|---|---|---|
| Create | 76689 (33385.0x faster) | 76567 (33438.2x faster) | 47102 (54355.7x faster) | 47395 (54019.7x faster) | 2560262853 |
| Update | 247101 (10703.3x faster) | 247021 (10706.8x faster) | 35819 (73838.1x faster) | 36029 (73407.7x faster) | 2644805261 |
| Delete | 180271 (7169.4x faster) | 181398 (7124.8x faster) | 94116 (13732.3x faster) | 100143 (12905.9x faster) | 1292432593 |
| Query All | 21580 (1.0x faster) | 21592 (1.0x faster) | 28388 (1.3x slower) | 28312 (1.3x slower) | 22126 |
| Query by Id | 55 (198863.3x faster) | 59 (185381.0x faster) | 1018 (10744.1x faster) | 1018 (10744.1x faster) | 10937480 |
| Query by Source | 1476 (110.8x faster) | 1453 (112.5x faster) | 480 (340.7x faster) | 478 (342.1x faster) | 163528 |
| Query by Target | 1514 (109.3x faster) | 1478 (111.9x faster) | 445 (371.8x faster) | 393 (420.9x faster) | 165432 |
Each Doublets cell is annotated with how many times faster (or slower) it is than SpacetimeDB for the same operation.
Doublets is an embedded store: an operation is a few pointer dereferences and tree rotations in memory (or in a memory-mapped file), while every SpacetimeDB operation is a reducer call over a WebSocket connection to a separate process, and every query is served from the client-side subscription cache. The measured difference is dominated by that architectural difference rather than by the data structures themselves.
To get fresh numbers, please fork the repository and rerun the benchmark in GitHub Actions.
| Operation | SpacetimeDB | Doublets United | Doublets Split |
|---|---|---|---|
| Create | O(log n) + network | O(log n) | O(log n) |
| Delete | O(log n) + network | O(log n) | O(log n) |
| Update | O(log n) + network | O(log n) | O(log n) |
| Query All | O(n) cache read | O(n) | O(n) |
| Query by Id | O(n) cache scan | O(1) | O(1) |
| Query by Source | O(n) cache scan | O(log n + k) | O(log n + k) |
| Query by Target | O(n) cache scan | O(log n + k) | O(log n + k) |
The algorithmic complexity is the same for volatile and non-volatile Doublets variants. The non-volatile variants have additional I/O overhead due to memory-mapped file writes (flushed to disk on drop).
- Rust nightly, pinned in
rust/rust-toolchain.toml(rustupinstalls it automatically) - SpacetimeDB CLI:
curl -sSf https://install.spacetimedb.com | sh
# Start the local SpacetimeDB server
spacetime start &
# Build and publish the links module
spacetime build --project-path rust/spacetime-module
spacetime publish --project-path rust/spacetime-module --yes benchmark-linkscd rust
# Full benchmark run (1000 links, 3000 background)
SPACETIMEDB_URI=http://localhost:3000 SPACETIMEDB_DB=benchmark-links \
cargo bench --bench bench -- --output-format bencher | tee out.txt
# Quick benchmark run (CI scale)
BENCHMARK_LINK_COUNT=10 BACKGROUND_LINK_COUNT=100 \
SPACETIMEDB_URI=http://localhost:3000 SPACETIMEDB_DB=benchmark-links \
cargo bench --bench bench
# Generate the results table and charts from out.txt
python3 out.py out.txt --results results.md
# Regenerate everything the CI publishes: results.md, docs/benchmarks/ charts
# and the results section of README.md
python3 out.py out.txt --results results.md --readme ../README.md \
--docs-dir ../docs/benchmarkscd rust
SPACETIMEDB_URI=http://localhost:3000 SPACETIMEDB_DB=benchmark-links cargo testcd rust
cargo fmt --all
cargo clippy --all-targets
# Unit tests for the results reporting pipeline (no benchmark run required)
python3 -m unittest test_out -v.
├── docs/
│ └── benchmarks/ # Benchmark charts published by CI and shown above
│ ├── bench_rust.png
│ └── bench_rust_log_scale.png
├── rust/
│ ├── spacetime-module/ # SpacetimeDB WASM module (links table + reducers)
│ │ ├── Cargo.toml
│ │ └── src/
│ │ └── lib.rs # Table definition and reducers using `spacetimedb` crate
│ ├── Cargo.toml # Package manifest with pinned dependencies
│ ├── doublets-patched/ # Local patches to doublets-rs for modern nightly compatibility
│ │ └── PATCHES.md # Documents why patches are needed and what was changed
│ ├── rust-toolchain.toml # Pinned Rust nightly toolchain
│ ├── rustfmt.toml # Rust formatting config
│ ├── out.py # Results table, charts and README update
│ ├── test_out.py # Unit tests for out.py
│ ├── results.md # Generated results table (committed by CI)
│ ├── src/
│ │ ├── lib.rs # Links trait, constants (BENCHMARK_LINK_COUNT, BACKGROUND_LINK_COUNT)
│ │ ├── module_bindings/ # spacetimedb-sdk client bindings for the links module
│ │ ├── spacetimedb_impl.rs # SpacetimeDB SDK client (implements Links)
│ │ ├── doublets_impl.rs # Doublets store adapters (implements Links)
│ │ ├── exclusive.rs # Exclusive<T> wrapper for interior mutability
│ │ ├── fork.rs # Fork<B> — benchmark iteration isolation
│ │ └── benched/
│ │ ├── mod.rs # Benched trait (setup/fork/unfork lifecycle)
│ │ ├── spacetimedb_benched.rs # Benched impl for SpacetimeDB
│ │ └── doublets_benched.rs # Benched impls for Doublets stores
│ └── benches/
│ └── bench.rs # Criterion benchmark suite (7 operations x 5 backends)
└── .github/
└── workflows/
└── rust-benchmark.yml # CI: test on Linux/macOS, benchmark, publish results
Unlicense — Public Domain

