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:
| Endpoint | Behaviour |
|---|---|
GET /hello | Returns { "message": "Hello World" } |
GET /users/:id | Echoes the path parameter as { "id": "..." } |
POST /users | Validates { 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:
| Framework | Runtime |
|---|---|
| Deno (no framework) | Deno |
| Thunder | Deno |
| Oak | Deno |
| Express | Node |
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
| OS | Windows 10 (x64) |
| CPU | Intel Core i7-10850H @ 2.70GHz |
| Logical cores | 12 |
| Node | 24.13.0 |
| Deno | 2.8.2 |
| Thunder | Huruf-Tech/thunder@main |
| Deno baseline | Deno.serve + Zod |
| Express | 5.2.1 |
| Oak | 17.1.x |
| Zod | 4.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
| Framework | Runtime | Req/sec | Latency p50 | Latency p99 |
|---|---|---|---|---|
| Deno (no framework) | Deno | 21,901 | 2 ms | 4 ms |
| Thunder | Deno | 14,677 | 3 ms | 5 ms |
| Oak | Deno | 12,961 | 3 ms | 6 ms |
| Express | Node | 10,474 | 4 ms | 7 ms |
GET /users/:id
| Framework | Runtime | Req/sec | Latency p50 | Latency p99 |
|---|---|---|---|---|
| Oak | Deno | 13,898 | 3 ms | 5 ms |
| Deno (no framework) | Deno | 13,398 | 3 ms | 6 ms |
| Thunder | Deno | 13,360 | 3 ms | 6 ms |
| Express | Node | 10,347 | 4 ms | 7 ms |
POST /users (with validation)
| Framework | Runtime | Req/sec | Latency p50 | Latency p99 |
|---|---|---|---|---|
| Deno (no framework) | Deno | 18,052 | 2 ms | 7 ms |
| Thunder | Deno | 12,384 | 3 ms | 9 ms |
| Oak | Deno | 11,091 | 3 ms | 10 ms |
| Express | Node | 8,148 | 5 ms | 10 ms |
Takeaways
- The raw
Deno.servebaseline 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.servebaseline - 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 /usersvalidation 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.mjsCustomize the load with flags:
node run.mjs --connections=100 --duration=20 --pipelining=1 --warmup=3The runner writes a machine-readable results/results.json and a formatted results/results.md.