forgedb 0.3.1

ForgeDB — an application database generator. Compiles a declarative .forge schema into tailored Rust database code, a TypeScript SDK, and a REST API.
Documentation
---
title: "Benchmarks"
description: "How ForgeDB's generated code compares to SQLite, redb, DuckDB, and PostgreSQL — measured at matched durability, always alongside on-disk footprint."
purpose: "orientation"
structure: "C"
---
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.

<Callout type="warning" title="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.
</Callout>

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`](/docs/config/storage/).

## 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 |

<Callout type="note" title="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.
</Callout>

## 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.

<Callout type="warning" title="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](/docs/config/storage/).
</Callout>

## 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

```bash
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`](/docs/config/storage/); the crash-safety model the barrier guarantees is [durability](/docs/features/durability/).