Benchmarks
Wrapper overhead vs raw Drizzle across latency and memory, measured with fair API-parity comparisons.
better-drizzle is not trying to beat raw Drizzle at being raw Drizzle. It is trying to stay close while giving you a higher-level repository API. The benchmark suite quantifies exactly that overhead - including where the overhead is real.
How these numbers are produced
Generated by bun run bench:report on SQLite in-memory, to isolate wrapper
overhead from I/O. Both sides of every pair are sampled in the same window,
alternating which one leads, and the median across samples is reported.
Measurement uses mitata's engine, the same one behind bun run bench.
Read the percentages, not the microseconds
Absolute timings drift with machine load - the same lookup reads 53 µs on an idle machine and 93 µs under load. The ratio between the two sides is stable across runs; the absolute values are specific to the machine that produced them. Reproduce locally rather than treating the µs column as a spec.
Latency
Per-operation latency at the API-parity level - raw Drizzle does the same work and returns the same shape.
Reads
| Operation | Drizzle | better-drizzle | Overhead |
|---|---|---|---|
| Point lookup | 74.94 µs | 74.46 µs | −0.6% |
| Filtered list | 256.28 µs | 268.41 µs | +4.7% |
| Relation graph | 10.24 ms | 1.12 ms | 9.1× faster |
| Relation counts | 33.62 ms | 34.17 ms | +1.6% |
| Active count | 68.52 µs | 74.23 µs | +8.3% |
| Exists | 62.90 µs | 65.39 µs | +4.0% |
| Offset pagination | 121.23 µs | 131.15 µs | +8.2% |
| Cursor pagination | 119.01 µs | 128.91 µs | +8.3% |
Writes
| Operation | Drizzle | better-drizzle | Overhead |
|---|---|---|---|
| Create + delete roundtrip | 119.92 µs | 125.25 µs | +4.4% |
| Update + reload | 57.92 µs | 59.94 µs | +3.5% |
Transactions
| Operation | Drizzle | better-drizzle | Overhead |
|---|---|---|---|
| Simple transaction | 138.22 µs | 147.90 µs | +7.0% |
| Multi-op transaction | 383.83 µs | 359.67 µs | −6.3% |
| Read-only transaction | 117.68 µs | 130.25 µs | +10.7% |
Memory
Heap delta across batches of operations, from bun run bench:memory.
Heap deltas are noisier than latency ratios, so these are the ranges observed across runs rather than a single snapshot.
| Batch | Heap overhead |
|---|---|
| Single read | −50% |
| Write | −18% to −25% |
| Mixed read | +165% to +210% |
| Transaction | +91% to +106% |
How to read this
- Reads land within ~9% of raw Drizzle, and several are within noise. Point lookups, filtered lists, and existence checks are effectively free.
- Relation loading is the standout win. The batched loader issues one query per relation node instead of the per-row work the equivalent raw code ends up doing, which is why the relation graph is ~9× faster rather than marginally slower. This gap grows with the number of parent rows.
- Cursor pagination is at parity. A populated page ordered by one primary key computes exact navigation metadata with one indexed query; empty pages and more complex shapes keep a probe when they need one. Note that both sides resolve
hasPrevious- the raw comparison does it with a correlatedexistssubquery rather than assuming it. An earlier revision of this suite let the raw side hardcode that value, which quietly compared one query against two. The manual data-only reference is still faster because it skips the navigation check entirely. - Writes are within ~5% at parity.
- Transactions are within ~11%. Nested savepoints are a better-drizzle-only capability with no raw Drizzle equivalent, so they are not part of the parity table.
- Memory is mixed. Single reads use materially less heap; mixed-read batches and transactions use more, because relation state, hooks, and savepoint tracking have to be held somewhere.
Comparison groups
The suite reports three views on purpose:
- API parity - the fair comparison. Same work, same shape (relations loaded, pagination metadata computed).
- Transaction parity - the same operations wrapped in a transaction.
- Manual Drizzle reference - intentionally lower-level raw queries (flat joins, no relation resolution, no pagination metadata). These are faster but do less.
Don't compare across groups
The manual reference does less work, so it is not a fair overhead claim. Use the API-parity group as the headline number - see parity.
PostgreSQL JSONB
Typed JSONB path filters have their own suite (bun run bench:jsonb) because they need a real PostgreSQL instance. It seeds 100,000 rows and asserts two things before timing:
- Row parity - both sides return the same rows.
- Plan parity - both sides reach them the same way, verified with
EXPLAINon each side.
The typed form compiles to the same index scan as the hand-written SQL; PostgreSQL folds the parameterised path into the indexed expression. better-drizzle adds one jsonb_typeof filter that the hand-written version omits, which is what keeps a row with a string where its neighbours have a number from failing the cast.
That suite is I/O bound, so the two sides land within run-to-run noise of each other. It exists to prove the query is the same, not to produce an overhead figure.