Skip to content

Benchmarks

Real numbers, measured honestly. All results below come from the two benchmark suites that ship in the repository, so you can reproduce every row on your own hardware:

bash
# Browser (WASM + OPFS — drives your installed Chrome headlessly)
pnpm --filter @taladb/web build
pnpm bench:web

# Node.js (native module)
pnpm --filter @taladb/node build
pnpm bench

Setup

TalaDBv0.9.0, release builds
Machine2018 MacBook Pro — Intel i5-8259U @ 2.30 GHz, 8 GB RAM
Browser runtimeChrome 150 headless · @taladb/web (WASM) · OPFS-backed
Node.js runtimeNode v22 · @taladb/node (napi) · file-backed database
Date2026-07-11

Latencies are the median of repeated timed iterations after warmup, with deterministic seeded data. Documents are realistic small records (~7 fields); vectors are 384-dimensional unit vectors — the output shape of all-MiniLM-L6-v2, the most common on-device embedding model. The browser suite drives the @taladb/web worker over its message protocol; the Node suite uses the raw N-API binding. In both cases that is the same path the taladb wrapper uses, so timings include everything an application pays.

This is deliberately modest hardware. On a recent Apple Silicon or desktop-class machine, expect meaningfully better numbers across the board.

Browser — WASM + OPFS

The browser is TalaDB's flagship runtime: the same Rust engine compiled to WebAssembly, persisting to the Origin Private File System from a Web Worker. Every timing below includes the page ↔ worker postMessage round-trip and JSON serialisation — the full cost an app pays. Measured in real headless Chrome with OPFS active.

Browser — document writes

The browser engine is redb running directly on an OPFS file, fsync-flushing every commit by default — so single writes carry a per-commit flush cost (hence ~900 ops/s) while batched insertMany amortises one flush across the batch. See the durability note below for how to trade that for throughput.

OperationDetailResult
insert (single doc)one transaction per call~900 ops/s
insertMany (batch 100)single transaction per batch~27k docs/s
insertMany (batch 1,000)single transaction per batch~43k docs/s
insertMany (batch 5,000)bulk ingest of 100k docs~57k docs/s
updateOne (by _id)point update, $set one field~714 ops/s
deleteOne (by _id)point delete~667 ops/s

Browser durability model

By default the browser OPFS engine fsyncs (flushes the OPFS access handle) on every commit — the same per-commit durability as Node.js — so a hard crash does not lose acknowledged writes. That per-commit flush is what the single-write number above measures. To trade it for throughput, open with durability: { flush_every_write: false } (redb Eventual — commits are batched) and call await db.flush() at "save now" moments (before checkout, on visibilitychange). Independently, the worker also writes a 500 ms-debounced snapshot to IndexedDB — but that is only the auxiliary copy other tabs read and the offline fallback; it is not the authoritative store on the OPFS path, and its debounce is tunable via durability: { flush_ms }.

Browser — query latency at 100,000 documents

OperationDetailResult
findOne by _idprimary-key point get100 µs
find, indexed equalitysecondary index, ~10 matches300 µs
find, indexed range ($gte)newest ~100 docs800 µs
find, unindexed fieldfull scan of 100k docs168 ms
count, unindexed equalityscan, 12.5k matches175 ms

Sub-millisecond operations pay the worker postMessage round-trip (~50–100 µs), so browser point reads land around 100–300 µs — still far below anything network-bound. Scans are actually faster than Node's here because the memory-resident engine reads no disk pages.

Browser — vector search (384-dim, cosine, top-10, flat)

Exact k-nearest-neighbour over all vectors — no approximation, no recall trade-off.

Collection sizefindNearest (median)
1,000 vectors5.3 ms
10,000 vectors35 ms
50,000 vectors170 ms
OperationDetailResult
findNearest + filter, 50k vectorsindexed pre-filter locale: "en" (10%), then rank162 ms
Vector ingest, 50k vectorsinsertMany with a live vector index~2.4k docs/s

Semantic search over a typical on-device corpus (1k–10k chunks) answers in ~35 ms or less — faster than a network round-trip to any cloud vector database, with zero data leaving the device.

A SIMD build ~halves this

Browser vector search currently trails native by ~2× (Node does 50k in 93 ms; the browser 170 ms) — not the algorithm, the instruction set. The WASM build doesn't yet enable simd128, so the 384-wide dot product runs one lane at a time. An A/B +simd128 build measured 50k at 81 ms and 10k at 17 ms — near-native parity. Shipping it safely (dual builds with runtime feature detection, to keep Safari 15.2–16.3 working) is the top browser-performance item on the roadmap.

Node.js — native

The @taladb/node native module (napi), against a file-backed database — every committed write is fsync-durable when the call returns.

Node.js — document writes

OperationDetailResult
insert (single doc)one transaction per call~48 ops/s
insertMany (batch 100)single transaction per batch~4.0k docs/s
insertMany (batch 1,000)single transaction per batch~19k docs/s
insertMany (batch 5,000)single transaction per batch~36k docs/s
updateOne (by _id)point update, $set one field~46 ops/s
deleteOne (by _id)point delete~46 ops/s

Batch your writes

Every individual write is a full ACID transaction — the ~48 ops/s ceiling for single-document calls is the cost of a durable fsync to disk, not of TalaDB's write path. The same machine sustains 36k docs/s when writes share a transaction. If you are inserting more than a handful of documents, use insertMany.

Node.js — query latency at 100,000 documents

OperationDetailResult
findOne by _idprimary-key point get25 µs
find, indexed equalitysecondary index, ~10 matches169 µs
find, indexed range ($gte)newest ~100 docs by publishedAt1.4 ms
find, two-sided range ($gte+$lt)~100-doc publishedAt window1.4 ms
find, unindexed fieldfull scan of 100k docs444 ms
count, unindexed equalityscan, 12.5k matches470 ms

Point gets and indexed lookups stay in the microsecond range at 100k documents — the B-tree index layout gives O(log n) lookups regardless of collection size. Since v0.9.0 a two-sided range is a single bounded index scan (the ~100-doc window costs the same 1.4 ms as the one-sided form, down from ~463 ms in v0.8.x). Unindexed queries fall back to a full scan; if a field appears in your filters regularly, createIndex turns a 444 ms scan into a 169 µs lookup — a ~2,600× difference.

Node.js — vector search (384-dim, cosine, top-10)

The default (flat) index is exact k-NN over all vectors. The v0.9.0 scan rewrite (byte-streaming scoring, hoisted query norm, partial top-k selection) made it roughly 2× faster than earlier releases:

Collection sizefindNearest (median)v0.8.x
1,000 vectors2.5 ms4.0 ms
10,000 vectors18 ms40 ms
50,000 vectors93 ms188 ms
100,000 vectors198 ms369 ms

Hybrid search — metadata pre-filter, then rank — is cheaper still, because filtered-out vectors are skipped before scoring when the filter field is indexed:

OperationDetailResult
findNearest + filter, 100k vectorsindexed pre-filter matching 10% of docs, then rank346 ms
Vector ingest, 100k vectorsinsertMany with a live vector index~4.5k docs/s

Index your filter fields

The hybrid pre-filter is an ordinary document query, so it benefits from secondary indexes exactly like find does — index the field you filter on.

Optional HNSW index (Node.js, since 0.8.3)

For larger corpora, @taladb/node ships an approximate HNSW index (createVectorIndex(field, { dimensions, indexType: 'hnsw' })):

Metric50,000 × 384-dim vectors
findNearest (HNSW)15 ms (vs 93 ms flat — ~6× faster)
Graph build (one-off)~36 min on this hardware (47 s at 10k)
recall@10, uniform random vectors41% — the adversarial worst case
recall@10, clustered vectors (embedding-like)100%

Read the recall rows carefully: uniform random vectors have no neighbourhood structure and are the known worst case for graph-based ANN. Real model embeddings are strongly clustered, where HNSW recall is excellent — but measure on your data before relying on it. Two operational caveats: the graph is built at createVectorIndex / upgradeVectorIndex time and is not updated by later writes (rebuild during idle periods after bulk ingests), and graph construction is CPU-intensive and superlinear — plan the one-off build cost. The flat index stays the right default for most on-device corpora; @taladb/web and React Native are currently flat-only.

Reading these numbers

  • Scaling — exact vector search is linear in collection size; document point lookups are logarithmic. Both behave predictably as your data grows.
  • Latency floor, not ceiling — the test machine is a 2018 dual-fan ultrabook. Treat these as conservative.
  • Durability is per-commit on both platforms by default — Node.js and the browser OPFS engine both fsync every commit (see the note above); opt into batched commits with durability: { flush_every_write: false } + db.flush() for throughput. The 500 ms debounce is only the browser's auxiliary IndexedDB snapshot, not the OPFS store.
  • Browser vs native — flat vector search is ~2× slower in WASM today purely for lack of simd128; a measured SIMD build closes the gap. Everything else is at parity or (for scans) faster in the browser.
  • React Native — not yet benchmarked. RN runs the same Rust core via JSI with a file-backed database (like Node.js), so expect broadly Node-like numbers scaled to the device CPU — but these are an expectation, not a measurement. A device-driven suite (running inside an app on a simulator/emulator) is planned; until it lands, there are deliberately no RN figures here.
  • Methodology — deterministic seeded data, warmup before measurement, medians reported, one process at a time on an otherwise idle machine. Read scripts/bench-web.mjs + scripts/bench-web/bench.browser.js (browser) and scripts/bench.mjs (Node) for the exact workloads.