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#
| Engine | Model | Why it's here |
|---|---|---|
| SQLite | Embedded, single-writer, row-store | The canonical embedded DB, closest to ForgeDB's write model. The primary comparison. |
| redb | Embedded, pure-Rust KV | Closest on deployment shape — generated Rust linking a substrate crate, no external process. |
| DuckDB | Embedded, columnar | Closest on storage model. Expected to win vectorized scans — showing that is honest. |
| PostgreSQL | Client/server, row-store | The 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 |
| DuckDB | 208 µ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):
| scenario | ForgeDB | SQLite | redb | DuckDB | PostgreSQL |
|---|---|---|---|---|---|
point_lookup | 3.48 µs | — | 0.62 µs | 87 µs | 26 µs |
index_probe | 3.67 µs | — | 1.11 µs | 83 µs | 28.9 µs |
reverse_fk | 36.9 µs | — | 3.94 µs | 106 µs | 32.4 µs |
m2m | 5.94 µs | — | 1.62 µs | 196 µs | 59.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:
| scenario | ForgeDB (before) | ForgeDB (now) | SQLite | redb | DuckDB |
|---|---|---|---|---|---|
scan_aggregate | 32.4 ms | 568 µs (186 µs w/ @projection) | 352 µs | 632 µs | 92 µs |
scan_sort_top10 | 32.6 ms | 37 µs (index) | 4.35 µs | 809 µs | 491 µ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:
| engine | on-disk |
|---|---|
| ForgeDB | 1.88 MB |
| DuckDB | 4.01 MB |
| SQLite | 4.80 MB |
| redb | 8.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:
| readers | reads/s (idle) | reads/s (+writer) | writer writes/s |
|---|---|---|---|
| 1 | 274 644 | 271 262 | 273 |
| 2 | 295 867 | 302 747 | 265 |
| 4 | 338 970 | 334 061 | 264 |
| 8 | 139 621 | 164 031 | 271 |
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 writerAll 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.