Benchmarks
Three-way head-to-head against Prisma 7.9 (@prisma/adapter-pg, relationJoins preview on) and Drizzle 0.45 (node-postgres relational API) on a local PostgreSQL 17.9 database reached over a Unix socket, plus a hand-written pg control arm so you can read ORM overhead directly instead of inferring it.
The ten scenarios below were measured on 2026-08-09 against turbine-orm 0.66.0; the extended suite and the latency sweep were measured against 0.67.0. The current release is 0.67.0. Nothing below has been adjusted to match anything, because quoting a stale measurement is honest and inventing a fresh-looking one is not. Read the table as the shape of the result, who leads which scenario and by roughly how much, rather than as the latency you will see. Every arm ran once per round, the arm order rotated every round, and each figure is the median across 200 rounds, taken as the median of three full runs. See Methodology for why that matters, and Reproduce to re-run it against the version you are actually installing.
TL;DR. Over a local socket there is no network round-trip, so per-query overhead is the entire signal and most numbers are sub-millisecond. The single most useful figure on this page: Turbine runs at 1.09x hand-written
pg, where Drizzle runs at 1.49x and Prisma at 1.86x (geometric mean over the eight scenarios with a raw control). Across all ten scenarios Turbine is 1.82x faster than Prisma 7.9 and 1.32x faster than Drizzle 0.45 by geometric mean. Turbine takes seven scenarios, Drizzle takes three (L2 nested, L3 nested, and streaming, which Drizzle wins outright by 24%), and Prisma takes none. Two of the ten are contested between the two harnesses and are claimed by neither side.Those ten are also a narrow set, and the extended suite says so: three of them are
findUnique, the largest non-stream result is 100 rows, and nothing measures a write beyond one atomic increment. Across all 24 shapes Turbine leads 17. The additions found a loss the ten could not see (deep pagination, where a named prepared statement hits PostgreSQL's generic-plan cliff) and overturned a recommendation they would have produced (relationLoadStrategy: 'batched'is fastest on a socket and slower by 1 ms of round-trip time). Read Latency before acting on any single number here.
The one we lose, stated plainly#
Drizzle beats Turbine on streaming by 24%: 40.81 ms against Turbine's 54.01 ms to drain 50,000 comments in batches of 1,000. This reproduced in every run across both harnesses, so it is not noise.
An earlier version of this page showed Turbine fastest at streaming (60.7 ms) and called the scenario a near-tie. That was wrong. It rested on a single Drizzle measurement of 65.8 ms which we have not been able to reproduce: Drizzle measured 38.9 to 48.3 ms across the runs here. The tell is the control arm. Hand-written keyset pagination in raw pg drains the same table in 42.77 ms, and Drizzle sits directly on top of it, which is where a thin query builder doing a keyset loop should sit. The old 65.8 ms figure was the outlier, and the near-tie label was an artifact of it.
The honest reading is that Turbine's cursor carries roughly 25% overhead above hand-written keyset pagination on a full-table drain, and Drizzle's carries approximately none.
What a single number does not capture, and what you are buying for that 25%:
- Correctness without a monotonic key. Keyset pagination requires
orderByon a unique column. Turbine's cursor works with anyorderBy. - Clean early break.
for await ... breakcloses server-side state and releases the connection deterministically. This scenario drains the whole table, which is the case that flatters keyset most. - Nested
withinside the stream. Cursor batches resolve a fullwithclause per batch.
Those are real, but they are tradeoffs, not a rebuttal. If you are draining a large table on a unique ascending key and nothing else, Drizzle is measurably faster at it today.
Methodology, and why the harness changed#
The previous numbers came from bench.ts, which measures each ORM in a contiguous block: 200 Turbine iterations, then 200 Prisma, then 200 Drizzle. Anything that drifts over the life of the process (JIT state, page cache, autovacuum, thermal headroom) gets charged to whichever arm happened to occupy that slice of wall clock, and there was no control arm to tell drift from signal.
bench-interleaved.ts runs every arm once per round, rotates the arm order every round, reports the median across rounds, and adds a hand-written raw pg control to most scenarios. Both harnesses were run: the old one twice, because it is the only apples-to-apples way to diff against the earlier table, and the new one three times, because those are the numbers published here. The two harnesses agree on every winner except atomic increment, which is reported as the near-tie it is.
Setup#
- Database. Local PostgreSQL 17.9 (Homebrew), Unix-socket connection, no network hop, dedicated freshly seeded database. Deliberately not a container: Docker on macOS routes through a VM and a forwarded TCP port, which adds a latency floor.
- Client. Apple Silicon MacBook Pro (Apple M5 Max), macOS 26.5.1, Node v24.18.0. Client and database are the same host.
- Schema. organizations / users / posts / comments.
- Data. 5 orgs, 1,000 users, 10,000 posts, 50,000 comments, deterministic and identical for every ORM,
ANALYZEd after load. Row counts are verified before any number is recorded and the harness refuses to report if they do not match. - Versions. turbine-orm 0.66.0,
@prisma/client7.9.0 +@prisma/adapter-pg7.9.0,drizzle-orm0.45.2,pg8.22.0. The competitor versions are deliberately unchanged from the 0.50.0 run, which is what makes the two comparable, but they are pinned rather than current: all three have shipped releases since they were last checked, so re-check before quoting these ratios anywhere that matters. - Runs. 200 rounds + 20 warmup per arm per scenario (streaming: 9 rounds + 1 warmup, each round drains 50K rows). Three full runs; the table is the median of the three medians.
- Pool. Plain
pg.Poolsize 10 for Prisma, Drizzle and the raw control. Turbine uses its internal pool at default size. - Relation strategy. Turbine's default is
'auto'(since 0.41). It does not engage anywhere in this suite: every scenario uses a limit of 50 or less, every relation used is to-many, and every FK in the fixture is indexed, so the plan is the byte-identical single-statement join on every scenario. Verified by statement counting.
The drift floor: read this before the table#
The identical raw pg control arm was run inside every scenario that has one, in all three runs. Its spread across those runs is the floor below which nothing on this page is a measurement.
| Scenario | raw control, 3 runs (ms) | spread |
|---|---|---|
| findMany flat | 0.210 / 0.207 / 0.212 | 2.4% |
| findMany L2 | 1.939 / 1.958 / 1.953 | 1.0% |
| findUnique PK | 0.053 / 0.060 / 0.064 | 20.8% |
| count | 0.044 / 0.045 / 0.047 | 6.8% |
| stream 50K | 46.59 / 50.97 / 53.22 | 14.2% |
| atomic increment | 0.073 / 0.107 / 0.095 | 46.6% |
| pipeline | 0.201 / 0.219 / 0.205 | 9.0% |
| hot findUnique | 0.033 / 0.043 / 0.033 | 30.3% |
Same SQL, same data, same process, no code change. On the multi-millisecond scenarios the floor is 1% to 14%. On the sub-0.15 ms scenarios it is 21% to 47%.
So every sub-0.15 ms figure below carries roughly one third uncertainty in its absolute value. That covers findUnique by PK, count, atomic increment, hot findUnique and the pipeline batch. The orderings on those rows are stable across all five runs and can be relied on. The millisecond values cannot be compared to another run's millisecond values at that resolution, in either direction, and any "regression" or "improvement" smaller than a third on those rows is not real.
Raw results, measured 2026-08-09#
Median wall-clock ms per operation (lower is better). Bold = fastest ORM. The raw pg column is the hand-written control.
| Scenario | Turbine 0.66 | Prisma 7.9 | Drizzle 0.45 | raw pg |
|---|---|---|---|---|
| findMany, 100 users (flat) | 0.189 ms | 0.254 ms | 0.248 ms | 0.164 ms |
| findMany, 50 users + posts (L2) (contested) | 2.379 ms | 4.289 ms | 1.804 ms | 1.779 ms |
| findMany, 10 users → posts → comments (L3) | 1.436 ms | 3.990 ms | 1.223 ms | n/a |
| findUnique, single user by PK | 0.038 ms | 0.083 ms | 0.085 ms | 0.037 ms |
| findUnique, user + posts + comments (L3) | 0.197 ms | 0.397 ms | 0.303 ms | n/a |
| count, all users | 0.043 ms | 0.076 ms | 0.058 ms | 0.044 ms |
| stream, iterate 50K comments (batch 1000) | 54.01 ms | 57.93 ms | 40.81 ms | 42.77 ms |
atomic increment, view_count + 1 (contested) | 0.073 ms | 0.114 ms | 0.079 ms | 0.060 ms |
| pipeline, 5-query dashboard batch | 0.183 ms | 0.386 ms | 0.366 ms | 0.194 ms |
| hot findUnique, 500x same shape | 0.029 ms | 0.064 ms | 0.073 ms | 0.033 ms |
Ratio vs fastest#
1.00x = fastest ORM on that scenario. Higher = slower.
| Scenario | Turbine 0.66 | Prisma 7.9 | Drizzle 0.45 |
|---|---|---|---|
| findMany, flat | 1.00x | 1.34x | 1.31x |
| findMany, L2 (contested) | 1.32x | 2.38x | 1.00x |
| findMany, L3 | 1.17x | 3.26x | 1.00x |
| findUnique, PK | 1.00x | 2.18x | 2.24x |
| findUnique, L3 | 1.00x | 2.02x | 1.54x |
| count | 1.00x | 1.77x | 1.35x |
| stream, 50K | 1.32x | 1.42x | 1.00x |
| atomic increment (contested) | 1.00x | 1.56x | 1.08x |
| pipeline, 5-query batch | 1.00x | 2.11x | 2.00x |
| hot findUnique | 1.00x | 2.21x | 2.52x |
Scenario wins: Turbine seven, Drizzle three, Prisma none. Prisma is slower than Turbine on all ten.
Two of the ten are contested and should not be read as leads by either side. This run used two independent harnesses and they disagree about L2 nested (interleaved: Drizzle 1.804 vs Turbine 2.379; contiguous: Turbine 1.34 vs Drizzle 2.03) and atomic increment (interleaved: Turbine 0.073 vs Drizzle 0.079; contiguous: Drizzle fastest, Turbine 1.50x). The tables above report the interleaved harness because it rotates arm order every round and so shares drift across arms instead of letting it land on one. Where the two designs disagree, the scenario is close enough that the design decides it, and the honest answer is a tie. The 0.50.0 run recorded this for one scenario; it is now two. Streaming is not one of them: Drizzle wins it by 24% in every run of both harnesses.
Overhead above hand-written pg#
Geometric mean over the eight scenarios with a raw control:
overhead vs raw pg | |
|---|---|
| Turbine 0.66 | 1.09x |
| Drizzle 0.45 | 1.49x |
| Prisma 7.9 | 1.86x |
This is the most defensible number on the page, and the one worth quoting. The control is measured in the same rounds as the ORMs, so it cancels most of the drift; it has an absolute floor (nothing can beat 1.00x); and unlike a competitor ratio it does not move when a competitor ships a release. Read it as: on these shapes, choosing Turbine over writing the SQL yourself costs about 9% of the query time.
Across all ten scenarios, geometric mean: Turbine is 1.82x faster than Prisma 7.9 and 1.32x faster than Drizzle 0.45.
What the numbers actually mean#
1. On a local socket, the ORM layer is the whole story#
With the network removed, the fastest single SELECT (findUnique by primary key) runs in ~0.05 ms, and the gap between ORMs is the gap in their JavaScript per-query work rather than measurement noise. A pooled remote database compresses all of this back toward the network floor.
Conclusion. A socket-local benchmark measures which layer is leanest. A pooled-remote benchmark measures whether that matters in production (usually only a little). Read both, and read them separately.
2. Turbine leads the simple and repeated-shape reads#
Flat findMany, both findUnique shapes, count, the 5-query pipeline and the hot 500x-same-shape loop all go to Turbine. Shape-keyed SQL template caching means a repeated query skips SQL generation entirely, and prepared statements let Postgres reuse the plan. On the hot path Turbine runs at ~34,500 ops/sec versus ~15,400 for Prisma and ~13,300 for Drizzle.
3. Drizzle leads both nested reads and streaming#
Drizzle's relational query builder leads L2 on the primary harness (1.804 ms vs Turbine's 2.379 ms) and leads L3 (1.223 ms vs 1.436 ms). L2 is contested: the contiguous harness reverses it, so neither side owns that scenario (see above). Drizzle also wins streaming outright, by 24% (see above). Turbine's json_agg nesting sits behind Drizzle and 1.8x to 2.8x ahead of Prisma on the nested shapes (1.80x on L2, 2.78x on L3, 2.02x on the nested findUnique).
The gap itself has since been root-caused, and it is one default. Splitting the L2 wall time into server, wire and client shows the plan shape is a wash: with the JSON encoding held equal, server time is 0.424 ms for Turbine's correlated subquery against 0.363 ms for Drizzle's LEFT JOIN LATERAL, and our client-side decode is faster than Drizzle's while decoding more bytes. The whole difference is that json_build_object writes every key name into every nested object of every row: 34% of the L2 payload is key strings, 153.8 KB against 102.1 KB.
Turbine already ships the encoding that removes them. jsonEncoding: 'positional' emits the same json_build_array shape Drizzle uses, and with it turned on Turbine wins both nested scenarios: 1.615 ms against Drizzle's 1.690 on L2, and 0.506 against 0.629 on L3. It is not yet the default, and deliberately so: positional decoding maps array positions back to field names from a build-time shape, which turns any drift into a silent wrong-field bug rather than a loud error, so it wants the differential fuzz treatment before it becomes what everyone gets. That is the planned path rather than a promise of a date.
One thing remains genuinely unexplained, and it is narrower than the sentence that used to sit here: the widening between runs. L2 was published at 1.21x in the 0.50.0 run and measures 1.32x here. It is not the 0.66.0 release, since a direct interleaved A/B of 0.65.0 against 0.66.0 on the same database found 0.66.0 equal or marginally faster on every shape. Turbine's absolute L2 number barely moved; the raw control and Drizzle each got about 10% faster and Turbine did not.
4. Pipeline batching is Turbine's clearest win#
The 5-query dashboard batch runs in 0.206 ms on Turbine versus 0.431 ms (Prisma) and 0.402 ms (Drizzle): 2.09x and 1.95x faster respectively, and level with the raw pg control at 0.205 ms. Turbine sends all five queries in a single TCP flush using the Postgres extended-query pipeline protocol; Prisma's $transaction([]) and Drizzle's db.transaction() run their queries sequentially, waiting for each reply before sending the next. On a local socket that serialization cost is small in absolute terms; on a high-latency connection it grows with every round-trip.
5. Prisma trails everywhere on a local socket#
Prisma 7 dropped its Rust engine, but the client still does meaningful per-query work in TypeScript. With no network to hide it, that overhead puts Prisma behind Turbine on all ten scenarios, at 1.84x hand-written pg. This is a socket-local finding, not a claim about production: over a pooled remote network the same Prisma queries land inside the noise floor.
Beyond the headline ten#
Measured against 0.67.0 on the same machine, same fixture and same interleaved harness. The ten scenarios above are skewed and it is worth saying how: three of them are findUnique, the largest non-stream result is 100 rows, nothing measures a write other than one atomic increment, and nothing measures aggregation, to-one relations, many-to-many or relation filters. That set flatters small hot reads, which is not where applications spend their time.
The suite now covers 24 shapes. Turbine is fastest on 17 of them. The additions changed the picture in both directions: they found a loss the old set could not see, and they overturned a recommendation the old set would have produced.
Large result sets: the thing the old suite was hiding#
At 100 rows the row decoder is invisible. At scale it dominates.
| Scenario | Turbine 0.67 | Prisma 7.9 | Drizzle 0.45 | raw pg |
|---|---|---|---|---|
| findMany, 5,000 comments | 4.462 ms (1.07x raw) | 5.156 (1.24x) | 4.837 (1.16x) | 4.174 |
| findMany, 50,000 comments | 44.751 ms (1.07x raw) | 55.451 (1.33x) | 50.391 (1.20x) | 41.829 |
| findMany, 1,000 rows x 39 columns | 4.551 ms (1.21x raw) | 6.087 (1.61x) | 6.118 (1.62x) | 3.772 |
Turbine runs within 7% of hand-written pg on a 50,000-row read where the others are 20% and 33% above it. The wide-table row isolates per-column cost specifically, on a deliberately 39-column table, and both competitors land at ~1.6x raw against Turbine's 1.21x.
Writes#
| Scenario | Turbine 0.67 | Prisma 7.9 | Drizzle 0.45 | raw pg |
|---|---|---|---|---|
| createMany, 1,000 rows | 6.780 ms (1.38x raw) | 39.530 (8.06x) | 17.031 (3.47x) | 4.903 |
| update by primary key | 0.094 ms | 0.141 | 0.103 | 0.081 |
| upsert on a unique column | 0.105 ms | 0.140 | 0.113 | 0.082 |
| delete by unique column | 0.084 ms | 0.123 | 0.086 | 0.068 |
Bulk insert is the widest margin in the whole suite: 5.8x Prisma and 2.5x Drizzle, and Turbine is the only arm within 2x of a hand-written UNNEST insert. The three single-row rows are honest ties between Turbine and Drizzle: the differences are 0.009, 0.008 and 0.002 ms against a 0.018 ms drift floor for that run. Both are clearly ahead of Prisma.
Relations the headline set never touched#
| Scenario | Turbine 0.67 | Prisma 7.9 | Drizzle 0.45 |
|---|---|---|---|
| to-one, 500 posts + author | 1.419 ms | 1.521 | 1.485 |
| many-to-many, 200 posts + tags | 2.341 ms | 4.983 | 2.902 |
relation _count, 100 users | 0.240 ms | 0.299 | 0.357 |
relation filter (some), 100 users | 0.200 ms | 0.268 | 0.252 |
groupBy + having, 5 groups | 0.701 ms | 0.756 | 0.720 |
Many-to-many is the widest non-write margin, and it is also the one place the result shapes differ, which is the finding rather than a caveat: Turbine derives the junction from its two foreign keys and returns post.tags[], while Prisma and Drizzle both need the junction as an explicit model or table here and return post.postTags[].tag, one level deeper, for the caller to flatten. Each arm is written the way that ORM's own docs prescribe. The _count and relation-filter rows carry the same kind of note in the harness output: Drizzle's relational API has neither primitive, so those arms are hand-written subqueries through its query builder.
On groupBy all three sit within a few percent of raw pg, and the Turbine-to-Drizzle difference is itself at the drift floor. Read that row as "no ORM has a problem here" rather than as a win.
A loss we can now name: deep pagination#
| Scenario | Turbine 0.67 | Prisma 7.9 | Drizzle 0.45 | raw pg |
|---|---|---|---|---|
| filtered, sorted 20-row page, offset 40 | 0.212 ms | 0.238 | 0.258 | 0.186 |
| the same query at offset 9,000 | 0.549 ms | 0.318 | 0.341 | 0.276 |
...with forceCustomPlan: true | 0.292 ms |
Turbine names its prepared statements, which is exactly what wins the hot repeated-shape reads above, because node-postgres skips Parse only for a statement already parsed by name. PostgreSQL promotes a named statement to a value-blind generic plan on its sixth execution, and with OFFSET bound as a parameter that generic plan cannot know the offset is 9,000.
This is a property of naming statements, not of Turbine, and the control arm settles it: the same hand-written pg query costs 0.541 ms named and 0.272 ms unnamed. Drizzle and Prisma escape the cliff only because they never name statements, which is the same reason they never get the Parse skip. If you paginate deeply on a hot endpoint, set forceCustomPlan: true on that query and you land within 6% of raw.
Latency: the axis a socket cannot show#
Every number above this line is measured over a Unix socket where a round trip is ~0.05 ms. That is not a footnote, because it changes the sign of some results. These were measured through an in-process TCP proxy that forwards to the same PostgreSQL socket and delays each chunk by RTT/2. It models latency, not bandwidth or jitter, and the 0 ms row runs through the proxy too so every comparison is like for like.
Dashboard, 5 independent queries (medians, ms)
| arm | 0 ms | 1 ms | 5 ms | 25 ms |
|---|---|---|---|---|
Turbine pipeline | 0.395 | 3.059 | 5.583 | 26.830 |
Prisma 7 $transaction | 1.133 | 18.346 | 35.169 | 183.841 |
| Drizzle transaction | 1.053 | 18.349 | 34.823 | 184.030 |
Turbine's pipeline advantage grows from 2.7x to 6.9x as the link lengthens, because it is one round trip against roughly seven (BEGIN, five queries, COMMIT). Neither competitor has independent-query pipelining. This is the clearest case on the page where a socket-local benchmark understates a real difference, and it understated it by 2.5x.
Nested read, 50 users + posts (medians, ms)
| arm | 0 ms | 1 ms | 5 ms | 25 ms |
|---|---|---|---|---|
Turbine join | 2.527 | 5.115 | 7.809 | 29.487 |
Turbine batched | 1.407 | 6.807 | 11.343 | 54.003 |
| Drizzle | 1.897 | 4.620 | 7.258 | 27.856 |
relationLoadStrategy: 'batched' inverts. On a socket it is the fastest strategy by 1.34x, which is exactly the recommendation a socket-only suite would have produced. By 1 ms of round-trip time it is already slower than the join, and at 25 ms it is 1.83x slower, because it buys its win with an extra round trip per relation. Turbine's default 'auto' staying on the join is the right call for anything on a real link, and if your deployment is not on a socket the thing to set is autoRoundTripMs, not a pinned strategy.
Point reads converge. At 25 ms every findUnique arm lands within 0.3 ms of every other and of raw pg (Turbine 26.509, Drizzle 26.753, Prisma 26.828, raw 26.526). Turbine's ~1.5x advantage on hot small reads is real and worth having, but it is a low-latency-link result, and publishing it without this row overstates it.
What this cannot show. The proxy models latency, not bandwidth. The 34% payload reduction jsonEncoding: 'positional' buys does not appear in these tables at all and would only show up on a bandwidth-constrained link, so the case for that change is stronger than anything here can demonstrate.
Caveats#
- These are socket-local numbers. They isolate per-query overhead and deliberately exclude the network that usually dominates production latency. For a networked, pooled database the deltas here shrink toward the noise floor.
- Read the drift floor before quoting any sub-0.15 ms figure. Those absolute values carry roughly a third uncertainty. Their orderings are stable; their millisecond values are not precise.
- All measurements are wall-clock from the client, including query execution and result serialization.
- Single machine. Client and database are the same host, so there is no round-trip and no pooler hop.
- Prisma's nested scenarios run on its
joinstrategy, deliberately.benchmarks/prisma/schema.prismaenables therelationJoinspreview feature, which per Prisma's docs makesjointhe client-wide default, and the harness never overridesrelationLoadStrategyon a query. Without the flag Prisma would use itsquerystrategy and lose the nested scenarios by a wider margin. We measured Prisma in its favorable configuration. - We did not test connection pool starvation, long transactions, or write-heavy workloads. Those are different benchmarks.
- The streaming scenario drains the full table. A scenario that breaks out after the first match would show cursor cleanup advantages that a single number cannot capture. It is also the shape that most flatters keyset pagination.
- Prisma 7.6 was not installed or measured in this run. An earlier version of this page quoted 7.6-to-7.9 improvement percentages; they are not reproducible from this data and have been removed rather than carried forward.
NODE_ENVis unset, which leaves Turbine's dev-mode cache cross-check guards armed. A control run withNODE_ENV=productionproduced Turbine numbers indistinguishable on every scenario, so the published figures are not depressed by those guards.- The headline ten were measured against 0.66.0; the extended suite and the latency tables against 0.67.0. They are separate runs on the same machine and fixture, not one table split in two, so compare within a section rather than across them.
- The many-to-many arms do not return the same shape. Turbine returns
post.tags[]; Prisma and Drizzle returnpost.postTags[].tag, one level deeper. Each arm is written the way that ORM's own documentation prescribes, and the difference is reported as a finding rather than normalized away. - Drizzle has no relation
_countand no relation-filter primitive, so those two arms are hand-written subqueries through its query builder. A different hand-written formulation would move that number. - The latency proxy models latency, not bandwidth or jitter. It delays each TCP chunk by RTT/2 and forwards to the same local socket. Payload size therefore costs nothing in those tables, which understates the case for
jsonEncoding: 'positional'and for any strategy that moves fewer bytes.
Reproduce#
Raw data, the full writeup and all three harnesses are committed to the repo:
benchmarks/RESULTS-0.66.0.mdthe headline-ten run in full, including the 0.65-vs-0.66 A/B and the harness-disagreement table.benchmarks/RESULTS-EXTENDED.mdthe extended suite and the latency sweep in full, including the per-arm notes and the drift floor for each run.benchmarks/RESULTS-0.50.0.mdthe previous (0.50.0) run, including its per-run drift tables.benchmarks/RESULTS.mdthe historical runs.benchmarks/bench-interleaved.tsthe headline-ten harness.benchmarks/bench-extended.tsthe extended suite.benchmarks/bench-latency.tsthe cross-RTT sweep.
createdb turbine_bench
cd benchmarks
npm install && npx prisma generate
DATABASE_URL="postgresql:///turbine_bench?host=/tmp" npx tsx seed-neon.ts
DATABASE_URL="postgresql:///turbine_bench?host=/tmp" npx tsx bench-interleaved.ts
DATABASE_URL="postgresql:///turbine_bench?host=/tmp" npx tsx bench-extended.ts
DATABASE_URL="postgresql:///turbine_bench?host=/tmp" npx tsx bench-latency.tsAny Postgres endpoint works: Neon, Vercel Postgres, Supabase, or a local socket. Expect the shape above on a local socket and a flatter, network-dominated table on a pooled remote database.