Skip to content

Benchmarks

Every number here comes from a script in the repo. Run them yourself — numbers nobody can reproduce are marketing, not measurement.

Terminal window
bun benchmarks/http.bench.ts # real sockets, all four frameworks
bun benchmarks/dispatch.bench.ts # Request -> Response, no socket
bun benchmarks/router.bench.ts # router lookup alone
bun benchmarks/image.bench.ts # Bun.Image, and the cost of the upload guard

Recorded on Bun 1.4.0 / Node 22.22, Apple Silicon, 2026-08-21.

Real sockets, 50 connections, autocannon, identical routes. Each framework runs on the runtime it is actually deployed on.

framework runtime static text req/s param + json req/s p99
hono bun 94,765 95,911 1 ms
oven bun 93,805 94,087 1 ms
fastify node 80,084 77,824 1 ms
express node 18,155 18,072 5 ms

Oven is about 5.2× Express and 1.21× Fastify, and level with Hono — 1.9% behind on the parameterised route and 1.0% behind on static text, both inside run-to-run variance.

Request in, Response out, with no socket involved. Express and Fastify cannot appear here — they are built on Node’s req/res and have no Request entry point.

scenario oven hono on Bun 1.2.23: oven hono
static root 1051 ns 406 ns 1258 ns 1086 ns
param + json 1255 ns 916 ns 1519 ns 1838 ns
404 miss 1228 ns 956 ns 1530 ns 1922 ns

This page used to say Oven dispatched faster than Hono on two of these three rows. On Bun 1.4 that is no longer true. Oven got about 17% faster; Hono got 2.0–2.7× faster and now leads every scenario. The likely cause is in Bun’s own release notes: 1.4 rewrote the RegExp engine, and Hono’s router is regular-expression driven where ours walks a radix tree by hand. A runtime improvement landing squarely on a competitor’s hot path is still a real result.

This is the point: the two tables do not rank the same way.

Hono dispatches 2.6× faster on a static root and 1.37× faster on a parameterised route. At the socket, those advantages collapse to 1.0% and 1.9%. Once a real connection exists, syscalls and TCP dominate and several hundred nanoseconds of framework overhead vanishes.

The dispatch table is a useful signal about where our own overhead lives, and a bad basis for choosing a framework. We publish both, because publishing only the flattering one would be dishonest — and publishing only the microbenchmark would mislead in the other direction. That was an easy sentence to write when the microbenchmark flattered us. It is the same sentence now that it does not.

Terminal window
git clone https://github.com/hiteshchoudhary/theoven.git
cd theoven && bun install
bun benchmarks/http.bench.ts

The harness starts each server as a separate process, waits for it to answer, runs autocannon against it, and shuts it down before the next — so no framework is measured while another holds memory or a JIT-warm CPU. Node contenders run under node, Bun contenders under bun, because comparing a framework on a runtime nobody deploys it to measures nothing.

Every server implements the same two routes and returns the same bytes:

benchmarks/servers/oven.server.ts
const app = createApp({ logger: silentLogger })
app.get('/', () => 'ok')
app.get('/users/:id', (ctx) => ({ id: ctx.params.id }))
app.listen(Number(Bun.env.PORT))

Numbers will differ on your machine — laptops thermally throttle, and the load generator shares a CPU with the server. What should hold is the ordering and the rough ratios. If it does not, that is worth an issue.

Every framework here is fast compared to a 2 ms database query. Oven is not trying to win a router microbenchmark; Bun makes everything fast. What we compete on is the time between bun create and an app shaped like production — and no benchmark measures that.

Caveats worth knowing: loopback only, empty handlers, and the load generator shares a CPU with the server, which compresses the top of the table. Treat anything above 90k req/s as “at the ceiling”.

Bun.Image, which is what the image brick is built on. The number that matters is not how fast a resize is — it is how much cheaper reading a header is than decoding the pixels, because that ratio is the whole basis for guarding an upload.

source on the wire header read resize + encode
800×600 (0.5 MP) 3 KB 0.8 µs 3.8 ms
2000×1500 (3.0 MP) 15 KB 0.4 µs 7.7 ms
4000×4000 (16 MP) 70 KB 0.6 µs 44.4 ms

Reading the header is constant time; decoding is linear in pixels. A 70 KB request body carrying 16 megapixels is refused in well under a microsecond, and the gap only widens as the attack gets bigger — which is why a byte limit alone is not a defence.

Image work also does not block the event loop: a 110 ms resize saw 87 timer ticks. A resize in a handler costs that request its latency, not the whole process’s.