Performance

typedb-lean vs the Rust driver

typedb-lean wraps TypeDB's official C driver, which is itself built on the Rust driver. The question is what the Lean layer costs. The answer: nothing measurable on scans, lookups and inserts, and less than the Rust baseline under concurrency.

1.09×scan throughput
vs Rust
+1.5%lookup p50
(within noise)
−36%lookup p99
tail latency
1.65×faster on 16
concurrent reads

Results

One run of lake exe bench on an Apple M-series laptop, TypeDB 3.14.0 in Docker, both clients on driver version 3.14.0, against the same server and the same data.

typedb-lean (Lean 4)typedb-driver (Rust)

Scan of 100,000 rows into structures

Lean 9% faster
rows per second · higher is better
typedb-lean
155,368
Rust
142,061

Point lookup, p50

on par
one read transaction per lookup · lower is better
typedb-lean
1.05 ms
Rust
1.03 ms

Point lookup, p99

Lean 36% lower
tail latency over 1,000 lookups · lower is better
typedb-lean
1.52 ms
Rust
2.39 ms

Bulk insert of 10,000 entities

on par
one write transaction, one insert query per entity · lower is better
typedb-lean
4.75 s
Rust
4.71 s

16 concurrent read transactions of 10,000 rows

Lean 1.65× faster
wall-clock time for all 16 · lower is better
typedb-lean
367 ms
Rust
607 ms
Workloadtypedb-leanRust driverLean / Rust
Scan, rows/s155,368142,0611.09
Lookup p501,048 µs1,033 µs1.02
Lookup p991,521 µs2,393 µs0.64
Bulk insert4,749 ms4,708 ms1.01
16 concurrent reads367 ms607 ms0.60

Workloads

The benchmark loads 100,000 person entities, each with an email key, a name and an age. Then it runs the same four workloads with this library and with a Rust program that uses typedb-driver directly (bench/rust). Both clients warm up first.

define
  attribute email, value string;
  attribute name, value string;
  attribute age, value integer;
  entity person, owns email @key, owns name, owns age;
-- The Lean side of the scan and the concurrent reads, from bench/Bench.lean
structure Person where
  e : String
  n : String
  a : Int64
  deriving FromRow

let rows ← d.runSync db .read (opts := { batchSize := 512 }) (query (α := Person) (scanQuery s.scan))

discard <| (EAsync.concurrentlyAll ((Array.range 16).map fun _ =>
  d.run db .read (opts := { batchSize := 512 }) (query (α := Person) (scanQuery 10000)))).block

Why a wrapper can match the native driver

Calling Rust through C from Lean adds a boundary crossing per call. The library makes those crossings rare and cheap:

The Rust baseline uses the driver's defaults. It is not tuned, so read the concurrency result as "the Lean layer adds no contention", not as "Lean is faster than Rust".

Targets and CI

The benchmark is a test, not just a report. It fails unless:

CI runs lake exe bench --ci on Linux x86_64 for pushes to main and pull requests: 20,000 rows, 300 lookups and 2,000 inserts, with the targets relaxed by another 10% for shared runners.

Reproduce

You need Docker, a Rust toolchain and protoc. Without TYPEDB_ADDRESS, the benchmark starts a typedb/typedb container at the pinned version and removes it afterwards.

lake exe bench              # full sizes, enforces the targets, writes bench/report.json
lake exe bench --ci         # reduced sizes, targets relaxed by 10%
lake exe bench --profile    # also times draining, fetching and decoding separately
TYPEDB_ADDRESS=127.0.0.1:1729 lake exe bench   # use a server you already run

The report has both clients' numbers side by side:

{"sizes": {"scan": 100000, "lookups": 1000, "bulk": 10000},
 "rust": {"scan_rows_per_s": 142060.9, "lookup_p50_us": 1032.5, "lookup_p99_us": 2393,
          "bulk_insert_ms": 4707.6, "concurrent_reads_ms": 607.3, ...},
 "lean": {"scan_rows_per_s": 155367.8, "lookup_p50_us": 1048.5, "lookup_p99_us": 1521.4,
          "bulk_insert_ms": 4749.2, "concurrent_reads_ms": 367.2, ...}}

Caveats