What does an ORM really cost you?

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.
The results
Section titled “The results”| 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
Section titled “Prisma”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:
INSERT INTO "public"."User" ("name","email","companyId") VALUES ($1,$2,$3), ($4,$5,$6), ...COMMITSo 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.
Run it yourself
Section titled “Run it yourself”git clone https://github.com/rogerpadilla/ts-orm-benchmark.gitcd ts-orm-benchmarkbun install
DATABASE_URL=postgres:///postgres npm run benchIt 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.