Skip to content

What does an ORM really cost you?

Bar chart of the time each ORM adds over the raw driver on a full PostgreSQL round trip, UQL lowest at 232µs and MikroORM highest at 2,236µs

My bench now does real stuff, full PostgreSQL round-trip operations.

Six ORMs, one real PostgreSQL, and a full lifecycle per pass: insert 10 rows, read 200 back with a filter and a sort, update one, read it again, load 50 parents with their children, delete, read the empty table. Every step timed separately, median of 250 interleaved iterations.

Entry Adds Total
UQL +232µs 1,621µs
Drizzle +685µs 2,074µs
TypeORM +838µs 2,227µs
Sequelize +1,185µs 2,574µs
Prisma +1,345µs 2,734µs
MikroORM +2,236µs 3,625µs

Adds is the number that matters, and it is the whole reason this benchmark works. Every entry pays the same PostgreSQL bill, so the totals compress into a 2.2x range and every ORM looks about the same. Subtract the floor, which is hand-written SQL with the rows mapped by hand at 1,389µs, and what is left is the ORM’s own contribution. That spans nearly 9.6x.

Everything above runs on node-postgres, so the driver is not the variable. Four of the six support nothing else.

Prisma is not slow across the board, and it does not finish last; it was MikroORM at 3,625µs against Prisma’s 2,734µs. Prisma’s nested read, the hardest step in the set, is the second fastest of any ORM here at 440µs. Its update, delete and single-row reads are all mid-field.

It is one step:

INSERT 10 rows
raw pg (hand-written) 454µs
UQL 488µs
MikroORM 625µs
Drizzle 635µs
Sequelize 643µs
TypeORM 668µs
Prisma 1,366µs

That one step is 2.0x the next slowest ORM and it is most of why Prisma places fifth. Turning on its query log shows what it sends:

What Prisma sends for createManyAndReturn
INSERT INTO "public"."User" ("name","email","companyId") VALUES ($1,$2,$3), ($4,$5,$6), ...
COMMIT

So createManyAndReturn wraps the batch in an explicit transaction, where the other five send one statement and stop. That is a fair thing to do and it is not the explanation: a BEGIN/COMMIT pair on this machine costs 88µs, and the gap to TypeORM, the next slowest here, is 698µs. The transaction is about an eighth of it. The rest is Prisma’s own overhead on the way in and out, which is the part you cannot opt out of.

Prisma 7 dropped its Rust query engine, and the client runtime is now TypeScript. This is the faster Prisma, not the old one.

The nested read is where ORMs are actually decided

Section titled “The nested read is where ORMs are actually decided”

It is the only step that loads a relation, and it has the widest spread of any read:

SELECT 50 parents with their children
raw pg (hand-written) 256µs
UQL 346µs
Prisma 440µs
TypeORM 498µs
Drizzle 535µs
Sequelize 680µs
MikroORM 1,205µs

This is the step worth caring about, because it is the one an ORM exists to do. Anyone can send an INSERT. Turning two result sets into an object graph without an N+1 is the actual job, and the field spans 3.5x on it.

Terminal window
git clone https://github.com/rogerpadilla/ts-orm-benchmark.git
cd ts-orm-benchmark
bun install
DATABASE_URL=postgres:///postgres npm run bench

It creates its own database and rewrites its own result tables, so the published numbers cannot drift from the last run. CI runs the full lifecycle with assertions on every push.

The latest set always lives on the benchmark page, and there is a feature-by-feature ORM comparison if speed is not your only axis. If your numbers come out different, open an issue.