Benchmarks

How ForgeDB's generated code compares to SQLite, redb, DuckDB, and PostgreSQL — measured at matched durability, always alongside on-disk footprint.

These are comparative benchmarks of ForgeDB's generated code against SQLite, redb, DuckDB, and PostgreSQL. ForgeDB compiles one schema into tailored Rust, so a get(id) is a direct index probe plus a column read. There is no query parser, no planner, and no network hop. That decides who it is fair to compare against and what is honest to measure.

Never read a number in isolation

Every result is reported alongside its durability setting and on-disk footprint. Throughput at a relaxed fsync tier is not comparable to throughput at a barrier tier — the difference is durability, not engine quality. The tables below label the tier of each row.

All numbers are macOS (Apple Silicon, APFS SSD), 2026-07-18/20. Relative shape is the signal; absolute fsync-bound numbers depend on the storage hardware.

Who we compare against#

EngineModelWhy it's here
SQLiteEmbedded, single-writer, row-storeThe canonical embedded DB, closest to ForgeDB's write model. The primary comparison.
redbEmbedded, pure-Rust KVClosest on deployment shape — generated Rust linking a substrate crate, no external process.
DuckDBEmbedded, columnarClosest on storage model. Expected to win vectorized scans — showing that is honest.
PostgreSQLClient/server, row-storeThe industry reference; carries parse + plan + socket cost we don't. Anchors "what a real RDBMS costs", not head-to-head.

PostgreSQL's read numbers are dominated by the client/server round-trip, so read them as the cost of a socket, not as head-to-head engine numbers.

Writes at matched durability#

A naive comparison shows a ~100× single-insert gap. Almost all of it is a durability mismatch, not engine speed. ForgeDB's default fsync is an F_FULLFSYNC device barrier on macOS (~3.5 ms); SQLite's out-of-box setting uses a weaker plain fsync. So writes are reported at two tiers, and you never compare across them.

Barrier tier (every engine fsyncs a real device barrier per commit):

Engine (barrier)insert_user
ForgeDB (fsync=always, default)3.89 ms
SQLite (fullfsync=1)3.90 ms
redb (immediate)3.94 ms

At the barrier tier the three are at parity — the barrier itself is the ~3.9 ms.

Relaxed tier (fsync deferred or batched — a real power-loss window):

Engine (relaxed)insert_user
ForgeDB (fsync=never)37 µs
SQLite (default, plain fsync)64 µs
DuckDB208 µs
redb (eventual)331 µs

At the relaxed tier ForgeDB-fsync=never is the fastest. That speed comes from dropping the barrier, which opens a window where a crash can lose recently-acknowledged writes — a durability trade, not an engine win. See [storage].fsync.

Group commit for bulk load#

A per-row insert pays one barrier per row, which is an anti-pattern for bulk load. Wrap the load in a transaction: staged rows use a buffered (no-fsync) WAL append and pay a single barrier at commit, with durability preserved.

bulk_load/10000 (fair, batched/grouped)time
ForgeDB (bulk_load_grouped)373 ms
SQLite (one-txn batch)851 ms
DuckDB (one-txn batch)1.33 s

Grouped ForgeDB beats SQLite and DuckDB at 10k rows at matched durability. Don't compare against the un-batched per-row path — that is the anti-pattern, not a fair number.

Reads and traversals#

Point and probe latency, all in-process (smaller is faster):

scenarioForgeDBSQLiteredbDuckDBPostgreSQL
point_lookup3.48 µs0.62 µs87 µs26 µs
index_probe3.67 µs1.11 µs83 µs28.9 µs
reverse_fk36.9 µs3.94 µs106 µs32.4 µs
m2m5.94 µs1.62 µs196 µs59.5 µs

redb's B-tree beats ForgeDB on point reads

redb wins point lookups and reverse-FK traversal — the gap is real. ForgeDB full-materializes each child record on reverse_fk, which is the largest residual. DuckDB loses every point op by 1–2 orders of magnitude, expected for a columnar analytical engine. PostgreSQL's latencies are the client/server round-trip, the cost that axis exists to show.

Scans and aggregates#

DuckDB is built to win these; showing that is honest. ForgeDB closed most of the gap with a column-pruned scan and an ordered index:

scenarioForgeDB (before)ForgeDB (now)SQLiteredbDuckDB
scan_aggregate32.4 ms568 µs (186 µs w/ @projection)352 µs632 µs92 µs
scan_sort_top1032.6 ms37 µs (index)4.35 µs809 µs491 µs

The old 32 ms was per-row syscalls in hashmap order, not a columnar-layout penalty. A plain aggregate now lands at 568 µs — ahead of redb, behind SQLite. A @projection that reads only the aggregated column drops it to 186 µs, which beats both. DuckDB's vectorized fold still leads. On the sorted top-N, ForgeDB's ordered index (37 µs) beats DuckDB's full scan. The ordered index covers non-nullable integer, timestamp, and decimal columns; f64 and nullable are deferred, and hash indexes are exact-match only.

Footprint#

On-disk bytes for a shared 41 500-row corpus, each engine via its simplest durable path:

engineon-disk
ForgeDB1.88 MB
DuckDB4.01 MB
SQLite4.80 MB
redb8.54 MB

ForgeDB has the smallest footprint — ~2.6× smaller than SQLite, ~4.5× smaller than redb. This is the flip side of the slower point reads: columnar files pack a column's values contiguously with no per-row B-tree page overhead.

Updates grow storage until compaction

Updates and deletes append superseding or tombstoned versions, so storage grows with churn. Measured: 2 000 updates to one user grew a table to 247.3 KB; in-process compact() reclaimed it to 84.3 KB (66%). See compaction.

Concurrency under a live writer#

Readers are lock-free. With one writer inserting continuously in the background and N reader threads hammering point reads, reader throughput stays within noise of its idle rate:

readersreads/s (idle)reads/s (+writer)writer writes/s
1274 644271 262273
2295 867302 747265
4338 970334 061264
8139 621164 031271

A live writer does not throttle reads. Two limits: this is one writer plus N readers (the v1 contract), and each reader sees a snapshot — rows appended after it captured its handle are invisible, which is the isolation guarantee, not a miss. Reader throughput scales to about 4 threads, then falls off at 8 past this host's core count.

Where ForgeDB wins and loses#

Wins: cold bulk load with no parser, matched-durability write parity, the fastest relaxed write tier, the smallest footprint, no-network latency, and generated relation traversal with no runtime join planner.

Loses (fine to show): point reads versus redb's B-tree; reverse-FK, which full-materializes each child today; analytical aggregates versus DuckDB's vectorized fold; and anything needing a range or prefix index beyond the ordered index, since hash indexes are exact-match only.

Running#

make bench            # embedded suites (ForgeDB + SQLite + redb + DuckDB)
make bench-footprint  # on-disk footprint + ForgeDB churn bloat
make bench-concurrency# reader throughput under a live writer

All targets run from the repo root. Full methodology, the configuration matrix, and per-engine cuts live in docs/BENCHMARKS.md. The durability lever behind the two write tiers is [storage].fsync; the crash-safety model the barrier guarantees is durability.

Search documentation

Find pages across the ForgeDB docs