Benchmarks

How Thunder's request throughput and latency compare with Express and Oak.

These benchmarks compare Thunder against two popular HTTP frameworks - Express and Oak - on a set of identical endpoints. The goal is to measure framework overhead: routing, request parsing, and validation, with no database in the hot path.

The full, runnable harness lives in the benchmarks/ directory of this repository. Every number below can be reproduced with a single command - see Reproducing the results.

What is measured

Each framework exposes the same three endpoints:

EndpointBehaviour
GET /helloReturns { "message": "Hello World" }
GET /users/:idEchoes the path parameter as { "id": "..." }
POST /usersValidates { name, email, age } and returns 201; invalid bodies return 400

Validation is done with Zod for Thunder and Oak, and with Zod inside the handler for Express - so every framework pays a comparable validation cost.

Runtimes

Thunder runs only on Deno, so the comparison is grouped by runtime:

FrameworkRuntime
Deno (no framework)Deno
ThunderDeno
OakDeno
ExpressNode

A raw Deno.serve handler (manual routing + Zod, no framework) is included as a baseline - it represents the floor of framework overhead on Deno. Express runs on Node as a cross-runtime reference point.

Compare frameworks within the same runtime (Deno vs Deno) for the fairest read. Cross-runtime numbers also reflect differences between the Deno and Node HTTP servers themselves, not just the framework.

Methodology

  • Load tool: autocannon
  • Connections: 50 concurrent
  • Duration: 10s per endpoint
  • Pipelining: 1
  • Warmup: 3s per server (discarded)

Each server is started on its own port, health-checked until it responds, warmed up, then load-tested per endpoint and shut down before the next contender runs.

Test environment

OSWindows 10 (x64)
CPUIntel Core i7-10850H @ 2.70GHz
Logical cores12
Node24.13.0
Deno2.8.2
ThunderHuruf-Tech/thunder@main
Deno baselineDeno.serve + Zod
Express5.2.1
Oak17.1.x
Zod4.4.3

Results are machine-specific and meant for relative comparison, not as absolute throughput claims. Numbers will differ on your hardware.

Results

Higher req/sec is better; latency is in milliseconds (lower is better). All runs completed with zero non-2xx responses and zero errors.

GET /hello

FrameworkRuntimeReq/secLatency p50Latency p99
Deno (no framework)Deno21,9012 ms4 ms
ThunderDeno14,6773 ms5 ms
OakDeno12,9613 ms6 ms
ExpressNode10,4744 ms7 ms

GET /users/:id

FrameworkRuntimeReq/secLatency p50Latency p99
OakDeno13,8983 ms5 ms
Deno (no framework)Deno13,3983 ms6 ms
ThunderDeno13,3603 ms6 ms
ExpressNode10,3474 ms7 ms

POST /users (with validation)

FrameworkRuntimeReq/secLatency p50Latency p99
Deno (no framework)Deno18,0522 ms7 ms
ThunderDeno12,3843 ms9 ms
OakDeno11,0913 ms10 ms
ExpressNode8,1485 ms10 ms

Takeaways

  • The raw Deno.serve baseline sets the ceiling. No framework can beat hand-written routing on the same runtime, so it is the fairest reference for how much overhead each framework adds.
  • Thunder leads the frameworks on Deno. It comes out ahead of Oak across the board while staying close to the raw Deno.serve baseline - impressive given that Thunder does more per request (file-based router discovery, per-route Zod shapes, and hook dispatch).
  • Validation is where Thunder holds up well. On the POST /users validation workload, Thunder edges out Oak and comfortably beats Express on Node, despite running full Zod parsing on every request.
  • Framework overhead is small relative to real work. With no database in the path, all four contenders sit in the same order of magnitude. In a real API, latency is dominated by I/O (database, network), so these differences shrink further in production.

Thunder optimizes for developer experience and type-safety (end-to-end Zod validation, generated OpenAPI specs and SDKs) while staying within range of leaner routers. For most APIs, the bottleneck is your database - not the framework.

Reproducing the results

From the benchmarks/ directory:

# 1. Clone the Thunder app (git-ignored) and give it a dummy DB URL.
#    Thunder connects to MongoDB lazily, so no real database is needed.
git clone --depth 1 https://github.com/Huruf-Tech/thunder.git frameworks/thunder
echo "DATABASE_URL=mongodb://localhost:27017/thunder-bench" > frameworks/thunder/.env

# 2. Install the Node-side dependencies (Express, autocannon).
npm install

# 3. Run the suite.
node run.mjs

Customize the load with flags:

node run.mjs --connections=100 --duration=20 --pipelining=1 --warmup=3

The runner writes a machine-readable results/results.json and a formatted results/results.md.

On this page