r/Backend • u/Worldly_Dish_48 • 1h ago
I got tired of "SELECT 1" microbenchmarks, so I tested 25 ORMs across 9 languages against a real 78M-row Postgres database. Here is what broke.
Every ORM benchmark I see online tests the same trivial query: SELECT * FROM users WHERE id = 1;
This doesn't benchmark an ORM. It benchmarks Linux loopback TCP socket round-trip time.
In production, ORMs don't fall over on primary-key lookups. They buckle when:
Deserializing 10,000 wide rows into typed structs under memory pressure.
Materializing 4-level deep entity graphs without Cartesian explosions or N+1 floods.
Compiling recursive CTEs, multi-frame window functions (
ROWS BETWEEN 4 PRECEDING), or pgvector cosine similarity.Squeezing 100 concurrent workers competing for row locks (
SELECT ... FOR UPDATE) through a constrained connection pool.Emitting 300 lines of incomprehensible internal template/typeclass garbage when a database column type changes.
To see how modern ORMs actually behave under load, I built a standardized, reproducible benchmark suite testing 25 ORMs across 9 programming languages (Go, Rust, C#, Java, Elixir, TypeScript, Python, Ruby, Haskell) against the official Postgres Air database (2024 edition, 78,232,673 real-world flight records on PostgreSQL 17 with pgvector).
Every single ORM was held to the exact same invariants: a strict 20-connection pool, zero raw string interpolation (native typed query builders only), and typed entity deserialization into domain records.
The Executive Leaderboard
| Rank | ORM | Language | Overall Score | Passed (HQ01–16) | Avg p95 Latency | Peak RPS (C=50) | Hydration Speed | Max Heap RSS | Blast Radius | Compiler UX |
|---|---|---|---|---|---|---|---|---|---|---|
| 🥇 1 | sqlc | Go 1.23 | 89.6 | 16/16 | 58.80 ms | 32,340 ops/s | 1,518,142 rows/s | 25.12 MB | 1 line | 10/10 |
| 🥈 2 | hasql | Haskell (GHC 9.10) | 89.5 | 16/16 | 65.83 ms | 277 ops/s | 199,025 rows/s | 13.03 MB | 1 line | 10/10 |
| 🥉 3 | Dapper | C# / .NET 8 | 89.5 | 16/16 | 62.67 ms | 264 ops/s | 1,279,038 rows/s | 87.27 MB | 1 line | 10/10 |
| 4 | jOOQ | Java 21 | 89.4 | 16/16 | 57.71 ms | 9,739 ops/s | 454,966 rows/s | 149.00 MB | 1 line | 10/10 |
| 5 | ent | Go 1.23 | 89.3 | 16/16 | 61.36 ms | 246 ops/s | 570,841 rows/s | 25.12 MB | 1 line | 10/10 |
| 6 | ecto | Elixir 1.17 / OTP 27 | 89.2 | 16/16 | 58.20 ms | 29,412 ops/s | 354,070 rows/s | 83.06 MB | 1 line | 10/10 |
| 7 | squeal | Haskell (GHC 9.10) | 89.1 | 16/16 | 187.03 ms | 258 ops/s | 15,271 rows/s | 25.17 MB | 1 line | 10/10 |
| 8 | ihp | Haskell (GHC 9.10) | 89.1 | 16/16 | 70.41 ms | 361 ops/s | 144,224 rows/s | 76.00 MB | 1 line | 10/10 |
| 9 | hibernate-orm | Java 21 | 89.1 | 16/16 | 62.66 ms | 256 ops/s | 564,130 rows/s | 15.00 MB | 1 line | 10/10 |
| 10 | efcore | C# / .NET 8 | 89.0 | 16/16 | 68.51 ms | 260 ops/s | 1,391,131 rows/s | 139.80 MB | 1 line | 10/10 |
| 11 | orville | Haskell (GHC 9.10) | 88.9 | 16/16 | 73.71 ms | 247 ops/s | 99,705 rows/s | 38.79 MB | 1 line | 10/10 |
| 12 | gorm | Go 1.23 | 88.9 | 16/16 | 58.50 ms | 256 ops/s | 712,657 rows/s | 25.37 MB | 1 line | 10/10 |
| 13 | rel8 | Haskell (GHC 9.10) | 88.8 | 16/16 | 132.32 ms | 254 ops/s | 122,678 rows/s | 18.75 MB | 1 line | 10/10 |
| 14 | persistent | Haskell (GHC 9.10) | 88.7 | 16/16 | 77.55 ms | 251 ops/s | 103,221 rows/s | 21.03 MB | 1 line | 10/10 |
| 15 | mikro-orm | TypeScript (Node 22) | 88.7 | 16/16 | 58.19 ms | 3,302 ops/s | 182,594 rows/s | 239.52 MB | 1 line | 10/10 |
| 16 | esqueleto | Haskell (GHC 9.10) | 88.6 | 16/16 | 82.16 ms | 250 ops/s | 85,377 rows/s | 25.22 MB | 2 lines | 10/10 |
| 17 | haskell-opaleye | Haskell (GHC 9.10) | 88.5 | 16/16 | 80.38 ms | 254 ops/s | 101,544 rows/s | 26.54 MB | 2 lines | 10/10 |
| 18 | beam | Haskell (GHC 9.10) | 88.4 | 16/16 | 13.57 ms | 241 ops/s | 113,504 rows/s | 22.39 MB | 6 lines | 10/10 |
| 19 | diesel | Rust 1.83 | 88.0 | 16/16 | 59.10 ms | 266 ops/s | 1,373,304 rows/s | 33.33 MB | 3 lines | 10/10 |
| 20 | typeorm | TypeScript (Node 22) | 87.8 | 16/16 | 55.99 ms | 235 ops/s | 385,422 rows/s | 71.55 MB | 1 line | 10/10 |
| 21 | rails | Ruby 3.3 (YJIT) | 87.5 | 16/16 | 85.34 ms | 247 ops/s | 89,792 rows/s | 150.00 MB | 1 line | 10/10 |
| 22 | sqlalchemy | Python 3.12 (asyncpg) | 87.4 | 16/16 | 72.57 ms | 5,718 ops/s | 279,239 rows/s | 92.50 MB | 1 line | 10/10 |
| 23 | prisma | TypeScript (Rust Engine) | 87.2 | 16/16 | 83.54 ms | 264 ops/s | 208,122 rows/s | 36.71 MB | 1 line | 10/10 |
| 24 | drizzle-orm | TypeScript (Node 22) | 86.8 | 16/16 | 64.45 ms | 242 ops/s | 498,613 rows/s | 184.58 MB | 4 lines | 10/10 |
| 25 | sea-orm | Rust 1.83 (sqlx) | 86.7 | 16/16 | 66.15 ms | 272 ops/s | 1,235,677 rows/s | 34.08 MB | 1 line | 10/10 |
The 5 Biggest Surprises
The Zero-Reflection Hydration Tier (> 1.2M rows/sec):
sqlc(1.52M rows/s),EF Core 8(1.39M rows/s),diesel(1.37M rows/s), andDapper(1.28M rows/s) operate in a different galaxy compared to reflection-heavy ORMs. Direct struct memory mapping and compiled pointer assignments destroy runtime reflection when reading wide tables.The BEAM Actor Model Destroyed Connection Contention: Under $C=100$ concurrent load on a 20-connection pool, most runtimes bottlenecked on thread pools or single event loops. Elixir’s
ectohandled pool checkout through isolated BEAM process mailboxes with near-zero queue latency, sustaining 29,412 requests/second.Node.js / V8 Event Loop Saturation: TypeScript ORMs (
drizzle,prisma,typeorm) performed well on single queries, but hit an unmistakable wall at $C=50..100$. Because V8 serializes and constructs thousands of JavaScript objects on a single thread, CPU time saturated the event loop, driving $p_{99}$ tail latencies beyond 500 ms and capping RPS around 225–264 ops/s.Haskell’s Memory Anomaly: Haskell’s
hasqlclocked the lowest resident memory footprint of the entire 25-ORM field at just 13.03 MB Peak Heap RSS, utilizing zero-copy PostgreSQL binary codecs.The Deliberate Mistake Suite (Compiler UX): We injected 10 deliberate compiler mistakes (type mismatches, dangling joins, nullability violations, scope leakage). While tools like
sqlc,squeal,hasql, andentcaught errors in 6–10 lines of clean output, complex typeclass/macro builders (rel8,drizzle) sometimes spewed 60–80 lines of internal framework plumbing.
Reproduce It / Add Your Favorite ORM
The benchmark repo contains all calculations, runner scripts, Docker setup, and the 25 JSON scorecards:
👉 GitHub Repository: https://github.com/tusharad/orm-benchmark
If you want to add an ORM (e.g. Kysely, Slonik, Bun, Peewee, SQLDelight), check out templates/generic/HARNESS_CONTRACT.md and submit a PR!
I'd love to hear your thoughts, critique of the methodology, or experiences running these tools in high-throughput production.