Turbine

Benchmarks

Measured on Turbine 0.92.0, October 2026

Turbine adds 9% to the time of hand-written SQL. Drizzle adds 59% and Prisma adds 113%.

hand-written pg1.00xbaseline
1Turbine1.09x+9%
2Drizzle1.59x+59%
3Prisma2.13x+113%
the SQL itself, the same for everyone what the library addsGeometric mean of 8 scenarios with a hand-written control.
  • 8.64x faster than Prisma and 3.25x faster than Drizzle on createMany, 1,000 rows
  • 1.87x faster than Drizzle on findUnique, user + posts + comments, in one round trip
  • 6.88x faster on a five-query dashboard 25 ms from the database

Each library runs the same queries as a hand-written pg control, on the same data, in the same rounds. It sends the same SQL with no ORM in between, so the distance from it is what each library costs you.

Every scenario#

24 common queries, from a primary-key lookup to a 50,000-row read. Each mark is that library's median time divided by Turbine's, so anything to the right of the blue line is slower than Turbine.

TurbineDrizzlePrismahand-written pg
Reads
findMany, 100 usersfindMany, 100 usersTurbine0.208 msDrizzle0.310 ms1.49xPrisma0.352 ms1.69xhand-written pg0.201 ms
findUnique by primary keyfindUnique by primary keyTurbine0.090 msDrizzle0.149 ms1.66xPrisma0.179 ms1.99xhand-written pg0.075 ms
count, all userscount, all usersTurbine0.079 msDrizzle0.114 ms1.44xPrisma0.169 ms2.14xhand-written pg0.078 ms
stream 50,000 comments, per rowtiestream 50,000 comments, per rowTurbine55.02 msDrizzle55.13 ms1.00xPrisma82.84 ms1.51xhand-written pg57.03 ms
hot findUnique, rotating idshot findUnique, rotating idsTurbine0.078 msDrizzle0.145 ms1.86xPrisma0.158 ms2.03xhand-written pg0.070 ms
findMany, 5,000 commentsfindMany, 5,000 commentsTurbine3.894 msDrizzle5.874 ms1.51xPrisma6.924 ms1.78xhand-written pg4.864 ms
findMany, 50,000 commentsfindMany, 50,000 commentsTurbine42.39 msDrizzle70.22 ms1.66xPrisma81.05 ms1.91xhand-written pg53.86 ms
findMany, 1,000 rows x 39 columnsfindMany, 1,000 rows x 39 columnsTurbine4.392 msDrizzle6.947 ms1.58xPrisma7.304 ms1.66xhand-written pg4.108 ms
list page, newest firstlist page, newest firstTurbine0.331 msDrizzle0.402 ms1.21xPrisma0.401 ms1.21xhand-written pg0.279 ms
Nested relations
findMany, 50 users + postsfindMany, 50 users + postsTurbine1.075 msDrizzle1.485 ms1.38xPrisma2.955 ms2.75xhand-written pg0.854 ms
findMany, 10 users + posts + commentsfindMany, 10 users + posts + commentsTurbine0.620 msDrizzle0.884 ms1.43xPrisma2.270 ms3.66x
findUnique, user + posts + commentsfindUnique, user + posts + commentsTurbine0.246 msDrizzle0.460 ms1.87xPrisma0.599 ms2.43x
to-one, 500 posts + authorto-one, 500 posts + authorTurbine1.555 msDrizzle1.992 ms1.28xPrisma2.174 ms1.40xhand-written pg1.265 ms
many-to-many, 200 posts + tagsmany-to-many, 200 posts + tagsTurbine1.477 msDrizzle2.487 ms1.68xPrisma3.810 ms2.58x
relation _count, 100 usersrelation _count, 100 usersTurbine0.376 msDrizzle0.492 ms1.31xPrisma0.527 ms1.40x
relation filter (some), 100 usersrelation filter (some), 100 usersTurbine0.282 msDrizzle0.409 ms1.45xPrisma0.464 ms1.65xhand-written pg0.319 ms
groupBy + having, 5 groupsgroupBy + having, 5 groupsTurbine0.968 msDrizzle1.005 ms1.04xPrisma1.095 ms1.13xhand-written pg0.929 ms
Writes
atomic incrementatomic incrementTurbine0.164 msDrizzle0.182 ms1.11xPrisma0.259 ms1.58xhand-written pg0.135 ms
createMany, 1,000 rowscreateMany, 1,000 rowsTurbine5.176 msDrizzle16.81 ms3.25xPrisma44.75 ms8.64xhand-written pg3.472 ms
update by primary keyupdate by primary keyTurbine0.166 msDrizzle0.184 ms1.11xPrisma0.263 ms1.58xhand-written pg0.138 ms
upsert on a unique columntieupsert on a unique columnTurbine0.210 msDrizzle0.214 ms1.02xPrisma0.282 ms1.34xhand-written pg0.146 ms
delete by unique columntiedelete by unique columnTurbine0.155 msDrizzle0.162 ms1.05xPrisma0.250 ms1.61xhand-written pg0.122 ms
Batches and aggregates
pipeline, 5-query batchpipeline, 5-query batchTurbine0.343 msDrizzle0.701 ms2.04xPrisma0.784 ms2.29xhand-written pg0.360 ms
first query on a new clienttiefirst query on a new clientTurbine0.501 msDrizzle0.502 ms1.00xPrisma0.685 ms1.37x
Medians in milliseconds. Further right is slower, on a log scale. A tie is too close to call between Turbine and Drizzle across runs.

Over a network, the gap grows#

On a local socket every query is cheap and the differences are the libraries' own CPU time. In production the database is milliseconds away, and round trips decide the result. A five-query dashboard shows it: Turbine's pipeline() sends all five queries in one round trip, and a transaction sends about seven.

Turbine pipeline()Drizzle transactionPrisma $transaction
050100150200ms0 ms1 ms5 ms25 msRound-trip time added between app and databaseTurbine 24.86Drizzle 171.0Prisma 171.1
At 25 ms, the transaction takes about 7 times as long. A local proxy adds the round-trip time, so every arm meets the same distance.

Streaming#

Drizzle 1.0.0-rc.446.99 ms
Turbine findManyStreamBatches48.22 ms
Turbine findManyStream (per row)52.54 ms
hand-written pg, keyset pages54.43 ms
Drizzle 0.45.354.96 ms
Draining 50,000 rows in batches of 1,000. Shorter is faster.

findManyStreamBatches finishes in 48.22 ms against 46.99 for the Drizzle 1.0 release candidate, a tie, and both Turbine APIs beat Drizzle 0.45.3. Turbine reads through a server-side cursor inside a transaction, while the Drizzle arms and the control page by key (WHERE id > $1 ORDER BY id LIMIT 1000), so the database work differs.

Where Turbine loses#

  • Deep pagination. A sorted 20-row page at offset 9,000 takes 0.759 ms on Turbine, 0.620 on Drizzle and 0.614 on Prisma. Turbine names its prepared statements, and PostgreSQL gives a named statement a generic plan that cannot see the bound offset. Set forceCustomPlan: true on that query and it runs in 0.568 ms.
  • Per-row streaming against Drizzle 1.0. findManyStream takes 52.54 ms against the release candidate's 46.99. Use findManyStreamBatches when you don't need rows one at a time.
  • relationLoadStrategy: 'batched' over a network. It is fast on a local socket and slower once the database is a network hop away, because it spends a round trip per relation. The default, 'auto', uses the join.
  • Ties. Scenarios marked tie, mostly single-row writes, are too close to call between Turbine and Drizzle across runs.

How it is measured#

  • Same rounds. Every arm, the hand-written control included, runs once per round, and the arm order rotates every round, so machine drift lands on all arms alike.
  • Medians. 200 rounds after 20 warmup for the core scenarios, fewer for the slowest ones, each taken as the median of 3 full runs.
  • Each ORM as its docs prescribe. Prisma 7.10.0 with @prisma/adapter-pg and the relationJoins preview, Drizzle 0.45.3 with its relational API, pg 8.23.1 under every arm. Drizzle has no relation _count or relation-filter primitive, so those two arms are subqueries through its query builder.
  • Same work in the control. Where a query returns nested rows, the hand-written version turns them into objects with Date timestamps, as every ORM does. On the 50-users-with-posts read Turbine takes 1.26x the hand-written time and Drizzle 1.74x. Turbine's widest gap to the control is createMany, 1,000 rows, at 1.49x.
  • Machine. A dedicated benchmark box, run by the repository's benchmarks.yml workflow: AMD Ryzen 9 7950X host, 4-vCPU 16 GB Firecracker VM, client and PostgreSQL 17 (Docker) on the same VM over a Unix socket. Node v24.18.0 and PostgreSQL 17.11, seeded with 5 orgs, 1,000 users, 10,000 posts and 50,000 comments, then ANALYZEd.
  • Large reads beat the control. On 50,000 rows Turbine takes 42.39 ms and hand-written pg 53.86. Turbine parses timestamps with its own faster parser, while the control uses node-postgres's default. Given the same parser, the control is faster than Turbine.
  • Small numbers. Figures under 0.15 ms carry about a third uncertainty in their absolute value. Their order held across runs; their margins are not quotable.

Run it yourself#

From the repository root, against any PostgreSQL database:

createdb turbine_bench
export DATABASE_URL="postgresql:///turbine_bench?host=/tmp"
 
npm install && npm run build      # the harnesses import the built package
cd benchmarks && npm install && npx prisma generate
npx tsx seed-neon.ts
cd .. && node dist/cli/index.js generate && cd benchmarks
 
npx tsx bench-interleaved.ts      # core scenarios
npx tsx bench-extended.ts         # more scenarios
npx tsx bench-stream-parity.ts    # streaming
npx tsx bench-latency.ts          # simulated round-trip times
npx tsx bench-network.ts          # a real link: use a direct, non-pooler endpoint

Or run the whole round on a fresh machine with gh workflow run benchmarks.yml, which uploads every log and the results file as an artifact. Expect this shape on a local socket, and a flatter one on a remote database, where the round trip dominates. The full write-up, with per-run figures: RESULTS-0.92.0.md.