Skip to content

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.

2.49×
Asena outruns NestJS out of the boxPlaintext — 202,066 vs 80,988 req/s.
3.76×Request validation
2.80×Database · read by id
2.24×Full API endpoint
Asena · Ergenecore vs NestJS · Express · Node.

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.

Plaintext
2.49×
faster than NestJS + Express on Node (1.54× on Bun)
1.74× against NestJS + Fastify on Node (1.47× on Bun)
202,066Asena · Ergenecore · Bun
80,988NestJS · Express · Node
131,281NestJS · Express · Bun
116,307NestJS · Fastify · Node
137,580NestJS · Fastify · Bun
Request validation
3.76×
faster than NestJS + Express on Node (3.10× on Bun)
2.95× against NestJS + Fastify on Node (2.75× on Bun)
134,793Asena · Ergenecore · Bun
35,873NestJS · Express · Node
43,497NestJS · Express · Bun
45,636NestJS · Fastify · Node
48,966NestJS · Fastify · Bun
Database · read by id
2.80×
faster than NestJS + Express on Node (2.67× on Bun)
2.36× against NestJS + Fastify on Node (2.56× on Bun)
78,023Asena · Ergenecore · Bun
27,883NestJS · Express · Node
29,178NestJS · Express · Bun
33,076NestJS · Fastify · Node
30,536NestJS · Fastify · Bun
Full API endpoint
2.24×
faster than NestJS + Express on Node (1.54× on Bun)
1.60× against NestJS + Fastify on Node (1.32× on Bun)
119,095Asena · Ergenecore · Bun
53,236NestJS · Express · Node
77,223NestJS · Express · Bun
74,513NestJS · Fastify · Node
90,256NestJS · Fastify · Bun

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.

ElysiaBun
213,772 req/s
HonoBun
202,099
Asena ErgenecoreBun
202,066
Asena HonoBun
190,030
FastifyBun
151,163
NestJS FastifyBun
137,580
ExpressBun
135,311
NestJS ExpressBun
131,281
NestJS FastifyNode
116,307
FastifyNode
92,134
ExpressNode
86,368
NestJS ExpressNode
80,988

JSON serialization ​

A small object built and serialized per request — no caching, no precomputed body.

ElysiaBun
199,070 req/s
Asena ErgenecoreBun
195,494
HonoBun
184,982
Asena HonoBun
178,857
FastifyBun
157,664
ExpressBun
136,565
NestJS FastifyBun
136,081
NestJS ExpressBun
119,195
NestJS FastifyNode
112,810
FastifyNode
94,322
ExpressNode
84,668
NestJS ExpressNode
82,459

Query string parsing ​

Four query parameters read, coerced to numbers, defaults applied, one arithmetic result.

ElysiaBun
184,765 req/s
HonoBun
179,141
Asena HonoBun
164,108
Asena ErgenecoreBun
159,116
FastifyBun
136,273
NestJS FastifyBun
121,404
ExpressBun
114,199
NestJS ExpressBun
108,660
NestJS FastifyNode
107,252
FastifyNode
91,254
NestJS ExpressNode
75,189
ExpressNode
74,745

Path parameters ​

Two path parameters parsed from the route and summed.

ElysiaBun
196,930 req/s
HonoBun
183,800
Asena HonoBun
182,279
Asena ErgenecoreBun
181,683
FastifyBun
145,609
ExpressBun
133,935
NestJS FastifyBun
122,280
NestJS ExpressBun
110,711
NestJS FastifyNode
106,239
FastifyNode
96,424
ExpressNode
83,006
NestJS ExpressNode
75,133

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.

ElysiaBun
144,270 req/s
HonoBun
138,948
Asena HonoBun
126,251
Asena ErgenecoreBun
119,095
FastifyBun
104,627
ExpressBun
93,518
NestJS FastifyBun
90,256
NestJS ExpressBun
77,223
NestJS FastifyNode
74,513
FastifyNode
68,088
ExpressNode
57,595
NestJS ExpressNode
53,236

Request validation ​

Each framework runs its own validator: Zod for Asena and Hono, TypeBox for Elysia, ajv for Fastify, class-validator for NestJS.

ElysiaBun
146,624 req/s
Asena ErgenecoreBun
134,793
HonoBun
134,307
Asena HonoBun
127,340
FastifyBun
118,503
ExpressBun
93,958
FastifyNode
75,359
ExpressNode
58,974
NestJS FastifyBun
48,966
NestJS FastifyNode
45,636
NestJS ExpressBun
43,497
NestJS ExpressNode
35,873

Database · read by id ​

One indexed SELECT against a 10,000-row table, connection pool of 20.

ElysiaBun
79,025 req/s
Asena ErgenecoreBun
78,023
Asena HonoBun
76,780
HonoBun
76,298
FastifyBun
64,006
ExpressBun
57,366
FastifyNode
47,572
ExpressNode
42,261
NestJS FastifyNode
33,076
NestJS FastifyBun
30,536
NestJS ExpressBun
29,178
NestJS ExpressNode
27,883

Static file · 4 KB ​

Each stack serves the file the way its own documentation tells you to.

ElysiaBun
139,629 req/s
Asena ErgenecoreBun
102,046
ExpressBun
64,727
NestJS ExpressBun
64,387
NestJS FastifyBun
60,862
FastifyBun
60,686
NestJS FastifyNode
50,646
NestJS ExpressNode
48,079
ExpressNode
47,319
Asena HonoBun
42,671
HonoBun
41,629
FastifyNode
39,285

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:

FrameworkOfficial middlewareHand-written handler
Hono45,128141,120
Elysia147,745116,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.

Express
+57%
Node 86,368Bun 135,311
Fastify
+64%
Node 92,134Bun 151,163
NestJS Express
+62%
Node 80,988Bun 131,281
NestJS Fastify
+18%
Node 116,307Bun 137,580

Asena's two adapters ​

Identical application code — the same controllers, services, validators and repositories. Only the adapter import changes.

ScenarioErgenecoreHono adapter
Plaintext202,066190,030
JSON serialization195,494178,857
Full API endpoint119,095126,251
Request validation134,793127,340
Database · read by id78,02376,780
Static file · 4 KB102,04642,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.

OperationRaw Bun.sqlDrizzleCost
read by id78,02339,462−49%
paged list48,06330,510−37%
insert73,09942,393−42%

Full results ​

Every measured number, req/s.

FrameworkRuntimePlaintextJSON serializationQuery string parsingPath parametersFull API endpointRequest validationDB read by idStatic 4 KB
ElysiaBun213,772199,070184,765196,930144,270146,62479,025139,629
HonoBun202,099184,982179,141183,800138,948134,30776,29841,629
Asena ErgenecoreBun202,066195,494159,116181,683119,095134,79378,023102,046
Asena HonoBun190,030178,857164,108182,279126,251127,34076,78042,671
FastifyBun151,163157,664136,273145,609104,627118,50364,00660,686
ExpressBun135,311136,565114,199133,93593,51893,95857,36664,727
NestJS FastifyBun137,580136,081121,404122,28090,25648,96630,53660,862
NestJS ExpressBun131,281119,195108,660110,71177,22343,49729,17864,387
FastifyNode92,13494,32291,25496,42468,08875,35947,57239,285
ExpressNode86,36884,66874,74583,00657,59558,97442,26147,319
NestJS FastifyNode116,307112,810107,252106,23974,51345,63633,07650,646
NestJS ExpressNode80,98882,45975,18975,13353,23635,87327,88348,079

Methodology ​

HardwareAMD Ryzen 9 9950X3D · 16C/32T · 60 GB RAM
RuntimesBun 1.4.0 · Node 26.7.0
Load generatorwrk, 12 threads, 400 connections, 60s
Repetitions2 per scenario, fresh server process each, spread reported
Warmup15s discarded before every measurement
Isolationserver pinned to cores 0-7, wrk to 16-31, single process, no clustering
DatabasePostgreSQL, pool size 20, 10,000 seeded rows, synchronous_commit = off
Loggingdisabled 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 — pg for Express and Fastify, Bun.sql for 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.sql prepares and caches per connection; pg and TypeORM issue unnamed statements and re-parse on every call. This favours the four Bun.sql applications 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.

Released under the MIT License.