Hono vs Express: Performance Benchmarks on Cloudflare Workers and Node
Hono has been eating Express's lunch for new TypeScript projects for the past 18 months, but most of the "comparisons" you'll find are either synthetic benchmarks that favor Hono by design or opinion pieces without numbers. Here's what we measured running both on real workloads, plus the architectural differences that actually matter.
Benchmark Setup
All tests run with wrk: 12 threads, 400 connections, 30-second duration. Same hardware (M3 Pro, 36GB RAM). Same Postgres connection pool (pg, 10 connections). Same query per route.
Test route: /users/:id — validates UUID param, runs a single SELECT by primary key, returns JSON.
Node.js Results (v22.4)
| Framework | Req/sec | Latency p50 | Latency p99 |
|---|---|---|---|
| Express 4 | 12,400 | 28ms | 94ms |
| Fastify 4 | 31,200 | 11ms | 38ms |
| Hono 4 | 28,600 | 12ms | 41ms |
| Hono 4 + Bun | 41,800 | 8ms | 27ms |
Cloudflare Workers Results
Express doesn't run on Workers (no Node.js runtime). Hono was designed for this environment.
| Configuration | Req/sec (simulated) | Cold Start |
|---|---|---|
| Hono on Workers | ~45,000 | <5ms |
| Hono on Workers + Durable Objects | ~38,000 | <5ms |
For comparison, running your own Node server on a VPS, even well-tuned, sees cold starts of 800ms-2s.
Routing: Trie vs Linear Scan
Express uses a linear route matching algorithm. It iterates through your registered routes in order until it finds a match. With 5-10 routes this is irrelevant. With 200+ routes (common in a real API), it's measurable.
Hono uses a RegExpRouter (default) and a TrieRouter (opt-in). Both are O(log n) or better for route matching.
In practice for most applications: this isn't why you'd choose Hono. The bigger differences are in the API design.
API Design Differences
Express (familiar, but shows its age)
import express from 'express';
const app = express();
app.get('/users/:id', async (req, res) => {
// req.params is string, you validate manually
const { id } = req.params;
if (!isValidUUID(id)) {
return res.status(400).json({ error: 'Invalid ID' });
}
const user = await db.users.findById(id);
if (!user) return res.status(404).json({ error: 'Not found' });
res.json(user);
});
app.listen(3000);
Hono (modern, typed, same pattern)
import { Hono } from 'hono';
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
const app = new Hono();
app.get(
'/users/:id',
zValidator('param', z.object({ id: z.string().uuid() })),
async (c) => {
const { id } = c.req.valid('param'); // typed, already validated
const user = await db.users.findById(id);
if (!user) return c.json({ error: 'Not found' }, 404);
return c.json(user);
}
);
export default app; // Works on Workers, Bun, Node, Deno
The key difference: c.req.valid('param') is typed. req.params in Express is Record<string, string>. The Hono version with zod-validator means your handler receives fully typed, validated data — the same pattern as tRPC but for REST.
Middleware: The Ecosystem Gap
Express has a decade of middleware on npm. Hono has official packages for the most common needs, but the long tail is thinner:
Hono has built-in/official:
- CORS, compression, logger, JWT auth, rate limiting, cache headers
- Zod validator, TypeBox validator
- Serve static files
- OpenAPI spec generation
You'll miss from Express:
-
passport.js(not yet ported cleanly) -
express-session(use Workers KV or Durable Objects instead) - Some specific OAuth middleware packages
For new projects, the official Hono middleware covers 90% of use cases. If you're migrating an Express app that relies on passport.js, factor that in.
The Multi-Runtime Story
This is where Hono wins unambiguously. The same Hono app runs on:
// src/app.ts - write once
import { Hono } from 'hono';
export const app = new Hono();
app.get('/health', (c) => c.json({ ok: true }));
export default app;
// For Cloudflare Workers — just export default
// wrangler.toml points to src/app.ts
// For Node.js
import { serve } from '@hono/node-server';
import { app } from './app';
serve({ fetch: app.fetch, port: 3000 });
// For Bun
import { app } from './app';
Bun.serve({ fetch: app.fetch, port: 3000 });
This portability is genuinely useful. We run Hono on Cloudflare Workers for our edge API (low-latency reads, cached) and the same codebase on a Node.js server for our internal API (where we need Postgres connections and can't use Workers' fetch-only model).
When to Keep Express
- You have an existing Express app with significant middleware investment — migration cost isn't worth it unless performance is a problem
- Your team is unfamiliar with the Web Request/Response API pattern (Hono uses it; Express has its own req/res objects)
- You depend on
passport.jsor other Express-specific middleware without Hono equivalents
When to Use Hono
- Greenfield TypeScript API — full stop, use Hono
- Anything deployed to Cloudflare Workers, Pages Functions, or edge runtimes
- You want end-to-end type safety without going all-in on tRPC
- Bun runtime (Hono + Bun is genuinely the fastest combination we've measured)
The Real Answer
The performance gap between Hono and Express on Node matters only at high concurrency. At 100 req/sec — which is where most production APIs live — you won't measure a difference in user-facing latency. Choose Hono because the DX is better and the types are tighter, not because of benchmarks.
Choose Hono over Fastify when you care about multi-runtime portability. Fastify is slightly faster than Hono on Node in our tests, but it doesn't run on Workers and has a more complex plugin architecture.
Top comments (0)