Same two JSON routes, Node v22.23.4, one cloud VM on November 2, 2026. Hono and Fastify land near each other. Express is the one this run separates.
On this page
The short answer
Hono vs Express on this VM is a large gap on an empty JSON route. The median for GET /json was 81,713.46 requests a second for Hono and 19,776.73 for Express, with p99 latency of 2 ms and 8 ms. Hono vs Fastify on the same route was close, 81,713.46 against 83,202.91, and the three-run ranges overlap. Stay on Express when the app already runs and the time goes to a database, not to this handler.
Hono vs Express performance
Hono vs Express performance, on this page, is one machine and two routes. Hono vs Express vs Fastify is that same run. People also search fastify vs express, so Fastify is in the table. It lives on the backend and stack hub. The reader is picking a Node or edge web framework, or deciding whether to leave an Express app.
I ran this on November 2, 2026, from 16:07:40 UTC to 16:12:01 UTC. Node v22.23.4, npm 10.9.9. The machine, from /proc/cpuinfo and /proc/meminfo: model name Intel(R) Xeon(R) Processor, 4 processors, kernel 6.12.94+, MemTotal 16,387,280 kB. lscpu reported KVM. This is one cloud VM, not a lab. The CPU line is the generic hypervisor string, not a SKU. Nothing pinned the cores, and another guest on the same host can take time. The three frameworks did not run at once. A rerun will not match these medians. Use the size of the gap, and the spread of the three runs, not a single number as the speed of the framework in production.
The script is scripts/benchmarks/hono-vs-express/bench.mjs. It installs pinned versions under /tmp/hono-vs-express-bench unless BENCH_WORK is set, copies the three server files, checks the JSON, then loads autocannon. Each route gets one warmup of 10 seconds at 100 connections, then three measured runs with the same settings. Pipelining is 1. The timeout is 10 seconds. The median is the middle of the three measured requests.average values. p99 is the middle of the three latency.p99 values. The warmup is in the CSV and is not in the median. Every run, warmup included, had 0 errors, 0 timeouts, and 0 non-2xx responses.
npm view that afternoon, and npm ls after the install, agreed on the versions. express 5.3.0, which was the latest tag. The latest-4 tag was 4.22.3, and this test did not install it. hono 4.13.16. @hono/node-server 2.1.6. fastify 5.12.8. autocannon 8.0.0.
Two routes, the same object on all three servers. GET /json returns {"message":"hello","ok":true"}. GET /users/42 returns {"id":"42","ok":true"}. The probe before each series required status 200 and that JSON, parsed. The raw bodies matched across Express, Hono, and Fastify: 29 bytes for /json and 21 bytes for /users/42.
The headers did not match, because I left each framework’s defaults. Express sent content-type: application/json; charset=utf-8, an etag, and x-powered-by: Express. Hono sent content-type: application/json and no etag and no x-powered-by. Fastify sent content-type: application/json; charset=utf-8 and no etag. Fastify was constructed with logger: false and no response schema. I did not turn Express’s etag off, and I did not add a Fastify schema to catch up. The Fastify routes page, read the same day, says setting a response schema allows 10 to 20 percent more throughput. That is their sentence. This run did not measure a schema.
autocannon’s own duration field on the measured runs was 10.01, 11, or 11.01 seconds, even though the setting was 10. The req/sec column is requests.average, the tool’s rate. I did not divide the total by 10.
| Server | Route | Median req/s | Run range | Median p99 |
|---|---|---|---|---|
| Express 5.3.0 | GET /json | 19,776.73 | 19,557.1 to 19,990.55 | 8 ms |
| Express 5.3.0 | GET /users/:id | 19,134.41 | 19,077.1 to 19,189.1 | 8 ms (7 to 8) |
| Hono 4.13.16, Node adapter 2.1.6 | GET /json | 81,713.46 | 80,660.37 to 82,009.61 | 2 ms |
| Hono 4.13.16, Node adapter 2.1.6 | GET /users/:id | 81,323.64 | 81,267.2 to 81,864.73 | 2 ms (1 to 2) |
| Fastify 5.12.8 | GET /json | 83,202.91 | 80,206.55 to 84,360.73 | 2 ms |
| Fastify 5.12.8 | GET /users/:id | 82,080 | 81,387.64 to 83,482.19 | 2 ms (1 to 2) |
-
19,776.73
Express GET /json, median req/s
-
81,713.46
Hono GET /json, median req/s
-
83,202.91
Fastify GET /json, median req/s
-
0
Errors, timeouts, and non-2xx responses, every run
81,713.46 divided by 19,776.73 is 4.13, to two decimal places. 83,202.91 divided by 19,776.73 is 4.21. On the path-param route, 81,323.64 divided by 19,134.41 is 4.25, and 82,080 divided by 19,134.41 is 4.29. Express’s highest /json run was 19,990.55. Hono’s lowest was 80,660.37. Fastify’s lowest was 80,206.55. Those ranges do not overlap. fastify vs express, on this handler, is that gap.
Hono vs Fastify is not that kind of gap. 83,202.91 divided by 81,713.46 is 1.018. Hono’s /json runs sat between 80,660.37 and 82,009.61. Fastify’s sat between 80,206.55 and 84,360.73. The intervals overlap. The path-param ranges overlap too. The median says Fastify, by a small amount. The spread says this VM does not know.
p50, from the same three measured runs, was 4 ms on both Express routes and 1 ms on both Hono routes and both Fastify routes. Every measured run landed on that integer. autocannon’s p99 values in this run are whole milliseconds too.
The @hono/node-server README, read November 2, 2026, says Hono is faster than Express and prints a bombardier comparison. Those figures are not this machine, not autocannon, and not these routes. They are not in the table.
This handler does no I/O. If the time is an ORM and a local database, that measurement is Drizzle vs Prisma, on SQLite, on this class of VM. It is not a request rate. The rows from this run are the benchmark CSV. Notes on the three server files are in the README. The same files are on Templates.
Runtime support
Hono’s Web Standards page says the framework uses Fetch objects, and lists Cloudflare Workers, Deno, Bun, Fastly Compute, AWS Lambda, Node.js via an adapter, Vercel, and WebAssembly through WASI. The getting started page asks you to pick a template. The list I was shown there includes nodejs, bun, and cloudflare-workers. The app object is the same. The import and the export change.
On Node, the Node.js guide says Hono was not designed for Node first, and that @hono/node-server wraps node:http. serve() returns the Node server. The guide lists Node 18.14.1+, 19.7.0+, and 20.0.0+, and says to use the latest of each major. The README in @hono/node-server 2.1.6 says “greater than 20.x”. The package.json I installed sets engines.node to >=20. This VM ran v22.23.4, which is inside >=20. I did not boot Node 18, so I am not picking between those two sentences with a test.
The Bun guide exports the app as the default and runs it with bun. On November 2, bun was not installed, and this benchmark did not time it. Bun vs Node on this VM is the later run.
The Cloudflare Workers guide is the same app, exported as default, deployed with Wrangler. I did not deploy it. Where a Worker sits next to Pages, and what a static site still does on Pages, is Workers or Pages. That post did not benchmark Hono.
Express 5.3.0’s installed Readme says it is a Node.js module and that Node.js 18 or higher is required. engines.node in that package is >= 18. I ran it on Node v22.23.4. I did not try Express on Bun, and I did not try it on Workers. The pages I read do not give Express a Workers or Bun entry point the way Hono’s guides do.
Fastify 5.12.8’s package.json does not set engines. The Long Term Support document shipped in that package, and the LTS page, lists Fastify 5.0.0 against Node.js 20 and 22. This run was Node 22. The same document says Fastify is also tested on alternative runtimes that are compatible with Node, and that those runtime teams own that testing. I did not run one.
API and middleware differences
Express is a chain of middleware. The middleware guide says a middleware function receives req, res, and next. If it does not end the response, it has to call next(), or the request hangs. app.get('/user/:id', (req, res) => ...) is the route form. The path param in the files I ran is req.params.id. res.json wrote the body. I did not mount express.json(), express.static, or any third-party middleware. There was no request body to parse.
Hono’s middleware guide separates a handler, which returns a response, from middleware, which awaits next() and returns nothing, or returns a response to stop. Registration is app.use or a method plus a path. A custom middleware in that guide is async (c, next) => { await next() }. Context values go through c.set and c.var when you type them with createMiddleware. The timed server mounts none of that. The routes call c.json, and the param is c.req.param('id').
Fastify’s middleware page says that since v3, middleware is not supported unless you register @fastify/express or @fastify/middie. I installed neither. The native place for work before the handler is a hook. The routes page lists onRequest, preValidation, preHandler, and the rest. The timed server uses none of them. The handler returns the object, and the param is request.params.id.
So the migration of these two routes is the handler and the param. It is not a middleware port. An Express app that is a stack of app.use calls does not paste into Hono, and it does not paste into Fastify without one of those plugins. I did not try the plugins, and I am not reporting a speed for them.
Migrating an Express route to Hono
These are the two files the benchmark executed, including the listen and the shutdown. The ports differ because the script started them one after another on fixed ports. A real app picks one port. No line was added for the article after the run.
Download express.mjs. Download hono.mjs. The Fastify file is linked in the next section.
import express from 'express';
const port = Number(process.env.PORT || 3101);
const app = express();
app.get('/json', (_req, res) => {
res.json({ message: 'hello', ok: true });
});
app.get('/users/:id', (req, res) => {
res.json({ id: req.params.id, ok: true });
});
const server = app.listen(port, '127.0.0.1', () => {
console.log(`listening ${port}`);
});
for (const signal of ['SIGINT', 'SIGTERM']) {
process.on(signal, () => {
server.close(() => process.exit(0));
});
}import { serve } from '@hono/node-server';
import { Hono } from 'hono';
const port = Number(process.env.PORT || 3102);
const app = new Hono();
app.get('/json', (c) => c.json({ message: 'hello', ok: true }));
app.get('/users/:id', (c) => c.json({ id: c.req.param('id'), ok: true }));
const server = serve(
{
fetch: app.fetch,
hostname: '127.0.0.1',
port,
},
(info) => {
console.log(`listening ${info.port}`);
},
);
for (const signal of ['SIGINT', 'SIGTERM']) {
process.on(signal, () => {
server.close(() => process.exit(0));
});
}What moved, and what I checked with the probe:
- The app is
express()ornew Hono(). The Hono process also importsservefrom@hono/node-server. On Bun or Workers that import goes away, and the Hono guides export the app instead. This file is the Node form, because that is what was timed. res.json({ message: 'hello', ok: true })becomesc.json({ message: 'hello', ok: true }). Both probes returned the same 29 bytes.req.params.idbecomesc.req.param('id'). Both probes returned{"id":"42","ok":true}.app.listen(port, '127.0.0.1')becomesserve({ fetch: app.fetch, hostname: '127.0.0.1', port }). The Node guide saysserve()wrapsnode:httpand returns the server. Closing it is on you. The signal handler does that.- Nothing in the Express file reads
reqexcept the param, and nothing writes a header by hand. The etag andX-Powered-Byshowed up anyway. The Hono file did not send them.
What does not move with these two routes: Express middleware, express.Router, res.send, and anything that needs the Node req and res objects. Hono on Node can reach those through c.env.incoming and c.env.outgoing, which the Node guide documents. This server does not. If the route’s work is a query, the framework line is the small part. The ORM measurement is the other post. The hosted backend behind the route is a third choice, below.
Hono vs Fastify
Hono vs Fastify, on this VM, is 83,202.91 against 81,713.46 on GET /json, and 82,080 against 81,323.64 on the param route. The ranges in the table overlap. I would not switch between them for that difference. fastify vs express is the comparison this benchmark actually separates.
Fastify’s file is the third server the script ran. Download fastify.mjs.
import Fastify from 'fastify';
const port = Number(process.env.PORT || 3103);
const app = Fastify({ logger: false });
app.get('/json', async () => {
return { message: 'hello', ok: true };
});
app.get('/users/:id', async (request) => {
return { id: request.params.id, ok: true };
});
await app.listen({ port, host: '127.0.0.1' });
console.log(`listening ${port}`);
for (const signal of ['SIGINT', 'SIGTERM']) {
process.on(signal, () => {
app.close().then(() => process.exit(0));
});
}The handler returns the object instead of calling a JSON helper. Fastify serializes it. Without a response schema, that is the default path, and the body still matched the other two servers byte for byte.
The reason to pick one when the rates overlap is the runtime and the surrounding code, not the median. Fastify’s handler speaks Fastify’s request and reply. The LTS document ties Fastify 5 to Node 20 and 22, plus whatever compatible runtime that ecosystem tests. Hono’s handler speaks a context over Fetch, and the same routes are what the Bun and Workers guides run. I did not run those guides. If the app is staying on Node, and you want hooks and a schema, Fastify is the library this table is next to. If the same routes have to load on Workers or Bun without a rewrite of the handler, Hono is the one whose docs show that entry point.
When to stay on Express
Stay when the Express app already runs and the handler is not this benchmark. p50 of 4 ms against 1 ms, and p99 of 8 ms against 2 ms, is the empty response. A database round trip, an auth check, or a call to a payment API is larger than that, and this page did not time one. Express 5.3.0 was the latest tag on November 2, 2026. Leaving it is a rewrite, not a patch.
- The app is already Express, the middleware stack is Express middleware, and you are not moving the process off Node: stay. This test did not port a middleware stack.
- The route waits on a database, a hosted API, or auth: stay until you have measured that route. The gap here is an empty JSON body.
- You are on Node, you want the faster empty handler, and you do not need Bun or Workers: Fastify and Hono both cleared Express by about four times on this VM, and they did not clear each other.
- The same handler should run on Cloudflare Workers or Bun: Hono’s guides show those entry points. This benchmark did not deploy them. The host choice is the Workers vs Pages post above.
- You still need a backend for the data, not a faster 200: that choice is Convex or Supabase. Neither server on this page called one.
The database under a route that does more than return JSON is its own measurement. Installing the three frameworks is a different question from how fast the handler is. Which package manager to install them with times npm, pnpm, Yarn classic, and bun install on one package.json.
Neon vs Supabase: which Postgres, and what it costs Hosted Postgres, free caps, and the bill when the project is quiet. Not a framework benchmark. Blog SQLite vs Postgres, timed on one machine A local file against a local server, including what happens when more than one worker writes. BlogFAQ
Is Hono faster than Express?
On this VM, for these two routes, yes. GET /json median was 81,713.46 requests a second for Hono 4.13.16 with @hono/node-server 2.1.6, and 19,776.73 for Express 5.3.0. p99 was 2 ms and 8 ms. GET /users/42 was 81,323.64 and 19,134.41. The three Express runs stayed under 20,000. The three Hono runs stayed over 80,000. That is an empty JSON handler on one cloud VM, not a route that waits on a database.
Can Hono replace Express?
For the two routes on this page, the Hono file is the replacement, and it returned the same JSON. It is not a drop-in for an app built on Express middleware. Express middleware is (req, res, next) and calls next(). Hono middleware takes the context and next, and awaits next() or returns a response. This test did not install a compatibility layer. A route that spends its time in a database will not feel the gap in this table.
Hono vs Fastify, which is faster?
On GET /json the medians were 83,202.91 for Fastify 5.12.8 and 81,713.46 for Hono. On GET /users/42 they were 82,080 and 81,323.64. Fastify’s three runs and Hono’s three runs overlap on both routes, so this VM does not settle which one is faster. Both medians are about four times the Express median. Fastify was started with logger off and no response schema.
Does Hono run on Node.js?
Yes. This benchmark is Hono on Node, through @hono/node-server 2.1.6, on Node v22.23.4. The Node.js guide says Hono was not designed for Node first and runs there with the adapter. That page lists Node 18.14.1+, 19.7.0+, and 20.0.0+. The installed adapter’s package.json sets engines.node to >=20, and its README says greater than 20.x. This run satisfies >=20. I did not run Node 18.
Hono vs NestJS?
This page did not install Nest and did not time it. The Nest first-steps page, read November 2, 2026, says Nest uses @nestjs/platform-express by default, can use a Fastify adapter, and also documents a node:http adapter that is not the default yet. That page does not mention Hono. Nest is a module and controller framework on top of an HTTP library. The servers timed here are the HTTP library.
Sources
- Node.js (Hono) Checked
- Getting Started (Hono) Checked
- Cloudflare Workers (Hono) Checked
- Bun (Hono) Checked
- Web Standards (Hono) Checked
- Middleware (Hono) Checked
- hono on npm Checked
- @hono/node-server on npm Checked
- Using middleware (Express) Checked
- express on npm Checked
- Routes (Fastify) Checked
- Middleware (Fastify) Checked
- Long Term Support (Fastify) Checked
- fastify on npm Checked
- autocannon on npm Checked
- First steps (NestJS) Checked