Benchmarks ​
Eight frameworks, two runtimes, eleven scenarios, one frozen contract. Every application returns byte-for-byte identical responses to byte-for-byte identical requests — verified mechanically before a single measurement was taken. Without that, a benchmark compares implementations, not frameworks.
Read the charts honestly: for a minimal app, Elysia and Hono win
Elysia and Hono are minimal routers, and on a minimal application they are the faster choice. The top of almost every chart on this page belongs to them, and nothing here disputes it — if your service is a handful of routes and you want the last few thousand requests per second, reach for one of those.
Asena is built for the application after that one: controllers, injected services, middleware, validation, lifecycle hooks, a container that wires it together. That is the shape NestJS gives you, and NestJS is the comparison this page is really about.
Asena vs NestJS ​
NestJS is the framework Asena is most often compared to: both are decorator-driven, both put an IoC container at the centre, both ask you to write controllers and inject services. The difference is what runs underneath.
How to read the four numbers on each card
nest new gives you NestJS on Express, running on Node. Nothing is chosen, nothing is tuned, and that is what the headline divides by — the configuration you actually get. The three smaller figures are the escapes, applied one at a time: swap Express for Fastify, move Node to Bun, or do both. None of them closes the gap, and every raw req/s behind them is printed on the card so you can check the arithmetic.
The database read is the one place an escape backfires: TypeORM is slower on Bun than on Node, so the Fastify line widens from 2.36× to 2.56× instead of narrowing.
Plaintext ​
A 13-byte text/plain response. Nothing but the HTTP layer and the router.
JSON serialization ​
A small object built and serialized per request — no caching, no precomputed body.
Query string parsing ​
Four query parameters read, coerced to numbers, defaults applied, one arithmetic result.
Path parameters ​
Two path parameters parsed from the route and summed.
Full API endpoint ​
Path param + query + custom header + a nested JSON body parsed, reshaped and re-serialized. Integer cents only, so no float formatting can diverge.
Request validation ​
Each framework runs its own validator: Zod for Asena and Hono, TypeBox for Elysia, ajv for Fastify, class-validator for NestJS.
Database · read by id ​
One indexed SELECT against a 10,000-row table, connection pool of 20.
Static file · 4 KB ​
Each stack serves the file the way its own documentation tells you to.
What the static numbers actually measure
The static scenario measures each stack's official static middleware, not the framework. A controlled A/B inside each framework — its own middleware versus a hand-written handler on the same server — makes that plain:
| Framework | Official middleware | Hand-written handler |
|---|---|---|
| Hono | 45,128 | 141,120 |
| Elysia | 147,745 | 116,895 |
Hono is not slow at serving files; serveStatic from hono/bun costs it 68%, because it runs two filesystem round-trips per request — a node:fs/promises stat() purely to test whether the path is a directory, then Bun.file().exists(). This is also why Asena's two adapters diverge here: Ergenecore implements static serving itself, while the Hono adapter delegates to that middleware.
Node vs Bun ​
The four Node-based frameworks were measured twice, running the same compiled dist/main.js on both runtimes. Only the runtime changes. This is the free speed-up a NestJS application can claim without rewriting a line — and the reason every card above prints its Bun variant alongside the Node one.
Asena's two adapters ​
Identical application code — the same controllers, services, validators and repositories. Only the adapter import changes.
| Scenario | Ergenecore | Hono adapter |
|---|---|---|
| Plaintext | 202,066 | 190,030 |
| JSON serialization | 195,494 | 178,857 |
| Full API endpoint | 119,095 | 126,251 |
| Request validation | 134,793 | 127,340 |
| Database · read by id | 78,023 | 76,780 |
| Static file · 4 KB | 102,046 | 42,671 |
Ergenecore leads on raw throughput and static files; the Hono adapter edges ahead on the body-heavy API endpoint. Pick Ergenecore for speed, the Hono adapter when you want Hono's middleware ecosystem.
What an ORM costs ​
Asena exposes the same three database operations twice: once over raw Bun.sql, once over @asenajs/asena-drizzle. Same queries, same response bytes, same driver — the only difference is the query builder.
| Operation | Raw Bun.sql | Drizzle | Cost |
|---|---|---|---|
| read by id | 78,023 | 39,462 | −49% |
| paged list | 48,063 | 30,510 | −37% |
| insert | 73,099 | 42,393 | −42% |
Full results ​
Every measured number, req/s.
| Framework | Runtime | Plaintext | JSON serialization | Query string parsing | Path parameters | Full API endpoint | Request validation | DB read by id | Static 4 KB |
|---|---|---|---|---|---|---|---|---|---|
| Elysia | Bun | 213,772 | 199,070 | 184,765 | 196,930 | 144,270 | 146,624 | 79,025 | 139,629 |
| Hono | Bun | 202,099 | 184,982 | 179,141 | 183,800 | 138,948 | 134,307 | 76,298 | 41,629 |
| Asena Ergenecore | Bun | 202,066 | 195,494 | 159,116 | 181,683 | 119,095 | 134,793 | 78,023 | 102,046 |
| Asena Hono | Bun | 190,030 | 178,857 | 164,108 | 182,279 | 126,251 | 127,340 | 76,780 | 42,671 |
| Fastify | Bun | 151,163 | 157,664 | 136,273 | 145,609 | 104,627 | 118,503 | 64,006 | 60,686 |
| Express | Bun | 135,311 | 136,565 | 114,199 | 133,935 | 93,518 | 93,958 | 57,366 | 64,727 |
| NestJS Fastify | Bun | 137,580 | 136,081 | 121,404 | 122,280 | 90,256 | 48,966 | 30,536 | 60,862 |
| NestJS Express | Bun | 131,281 | 119,195 | 108,660 | 110,711 | 77,223 | 43,497 | 29,178 | 64,387 |
| Fastify | Node | 92,134 | 94,322 | 91,254 | 96,424 | 68,088 | 75,359 | 47,572 | 39,285 |
| Express | Node | 86,368 | 84,668 | 74,745 | 83,006 | 57,595 | 58,974 | 42,261 | 47,319 |
| NestJS Fastify | Node | 116,307 | 112,810 | 107,252 | 106,239 | 74,513 | 45,636 | 33,076 | 50,646 |
| NestJS Express | Node | 80,988 | 82,459 | 75,189 | 75,133 | 53,236 | 35,873 | 27,883 | 48,079 |
Methodology ​
| Hardware | AMD Ryzen 9 9950X3D · 16C/32T · 60 GB RAM |
| Runtimes | Bun 1.4.0 · Node 26.7.0 |
| Load generator | wrk, 12 threads, 400 connections, 60s |
| Repetitions | 2 per scenario, fresh server process each, spread reported |
| Warmup | 15s discarded before every measurement |
| Isolation | server pinned to cores 0-7, wrk to 16-31, single process, no clustering |
| Database | PostgreSQL, pool size 20, 10,000 seeded rows, synchronous_commit = off |
| Logging | disabled in every application; no compression, no CORS, no ETag hashing |
Byte-level conformance is the gate. Before any target is measured it must return the exact expected bytes for all eleven contract requests plus the negative cases. A target that fails is skipped, not measured.
Validation runs on one route only. Adding a schema elsewhere would make some apps measure "parse + validate" and others "parse" while still returning identical bytes. Three applications had done exactly that and were corrected before this run.
Where these numbers are soft ​
- The database layer is not uniform. Each stack uses its idiomatic choice —
pgfor Express and Fastify,Bun.sqlfor Elysia, Hono and Asena, TypeORM for NestJS. The clean framework-to-framework database signal is the set of rows sharing one layer. - Prepared statements differ by driver.
Bun.sqlprepares and caches per connection;pgand TypeORM issue unnamed statements and re-parse on every call. This favours the fourBun.sqlapplications on the database reads. - The 256 KB static scenario is noisy — up to 18% spread between repetitions, because it moves 3-5 GB/s and is dominated by page cache and scheduler behaviour rather than framework code. Treat it as an order of magnitude, not a ranking.
- The load generator shares the machine with the server. Core pinning limits the interference but does not remove it.
The full contract, the harness and every raw result are reproducible from a single command.