Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 25 additions & 11 deletions benchmarks/fetch/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Benchmark

Benchmark to compare performance between the published npm version and local development version of @hono/node-server.
Benchmark comparing the published npm version and local development version of @hono/node-server with srvx.

This benchmark uses a basic Fetch API-based application without the Hono framework to measure the raw performance of @hono/node-server's adapter.

Expand All @@ -19,28 +19,42 @@ pnpm run benchmark

## What's Being Tested

Tests three endpoints:
Tests four endpoints:

1. **Ping (GET /)**: Simple response
2. **Query (GET /id/:id)**: Path parameter and query parameter handling
3. **Body (POST /json)**: JSON body processing
4. **Headers (GET /headers)**: Isolated `request.headers.get()` access

Each endpoint is tested with 500 concurrent connections for 10 seconds, measuring requests per second (Reqs/sec).

## Benchmark Environment

- **Machine**: Lenovo LOQ 15IRX9 (83DV)
- **CPU**: Intel Core i5-13450HX (10 cores, 16 threads)
- **Memory**: 24 GB
- **OS**: Arch Linux x86_64 (kernel 7.1.6)
- **Node.js**: 24.19.0

Comment on lines +31 to +38

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know which machine we get benchmark data from, my numbers were lower than the benchmark recorded one, so I added the env info
once we figure this out, I can replace it

## Understanding Results

Last updated: 2026-08-09

```
| Benchmark | npm | dev | Difference |
| ----------------- | -------------- | -------------- | ----------- |
| Average | 111,514.97 | 115,234.56 | +3.34% |
| Ping (GET /) | 122,207.70 | 125,678.90 | +2.84% |
| Query (GET /id) | 106,624.16 | 110,123.45 | +3.28% |
| Body (POST /json) | 105,713.04 | 109,901.23 | +3.96% |
| Benchmark | @hono/node-server (2.1.0) | srvx (0.12.5, fast) | @hono/node-server (dev) | dev vs npm | dev vs srvx |
| ----------------- | ------------------------- | ------------------- | ----------------------- | ---------- | ----------- |
| Average | 83,588.79 | 89,245.89 | 88,398.73 | +5.75% | -0.95% |
| Ping (GET /) | 87,502.45 | 97,875.62 | 96,320.69 | +10.08% | -1.59% |
| Query (GET /id) | 92,967.16 | 89,524.22 | 93,474.95 | +0.55% | +4.41% |
| Body (POST /json) | 72,621.78 | 75,968.80 | 73,823.52 | +1.65% | -2.82% |
| Headers (GET) | 81,263.78 | 93,614.90 | 89,975.77 | +10.72% | -3.89% |
```

- **npm**: Published npm version (`@hono/node-server`)
- **dev**: Local development version (from repository root `dist/`)
- **Difference**: Performance difference (positive values indicate improvement, negative values indicate regression)
- **@hono/node-server (2.1.0)**: Published npm version
- **@hono/node-server (dev)**: Local development version (from repository root `dist/`)
- **srvx (0.12.5, fast)**: Published npm version using its opt-in `FastResponse`
- **dev vs npm**: Development Hono compared with published Hono
- **dev vs srvx**: Development Hono compared with srvx

## Reference

Expand Down
4 changes: 2 additions & 2 deletions benchmarks/fetch/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"benchmark": "node --experimental-strip-types scripts/bench.ts"
},
"dependencies": {
"@hono/node-server": "^1.19.9",
"@hono/node-server-dev": "file:../.."

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pnpm was resolving a stale link some how, directly importing from ../../../dist/index.mjs works for dev case

"@hono/node-server": "^2.1.0",
"srvx": "^0.12.5"
}
}
79 changes: 56 additions & 23 deletions benchmarks/fetch/scripts/bench.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { spawn } from 'node:child_process'
import { once } from 'node:events'
import { setTimeout } from 'node:timers/promises'

const PORT = 3000
Expand All @@ -16,6 +17,7 @@ interface ServerResult {
ping: number
query: number
body: number
headers: number
}

async function waitForServer(): Promise<void> {
Expand Down Expand Up @@ -44,6 +46,15 @@ async function retryFetch(url: string, options?: RequestInit, retries = 0): Prom
}
}

async function stopServer(server: ReturnType<typeof spawn>): Promise<void> {
if (server.exitCode !== null || server.signalCode !== null) {
return
}
const exited = once(server, 'exit')
server.kill('SIGKILL')
await exited
}

async function testEndpoints(): Promise<void> {
// Test GET /
const res1 = await retryFetch('http://127.0.0.1:3000/')
Expand Down Expand Up @@ -75,6 +86,17 @@ async function testEndpoints(): Promise<void> {
`Body: Result not match - expected ${JSON.stringify(body)}, got ${JSON.stringify(json3)}`
)
}

// Test an isolated incoming request-header read.
const res4 = await retryFetch('http://127.0.0.1:3000/headers', {
headers: {
'x-test': '123',
},
})
const text4 = await res4.text()
if (res4.status !== 200 || text4 !== '123') {
throw new Error(`Headers: Result not match - expected "123", got "${text4}"`)
}
}

async function runBenchmarkForServer(
Expand Down Expand Up @@ -102,6 +124,7 @@ async function runBenchmarkForServer(
{ name: 'GET /', url: 'http://127.0.0.1:3000/' },
{ name: 'GET /id/:id', url: 'http://127.0.0.1:3000/id/1?name=bun' },
{ name: 'POST /json', url: 'http://127.0.0.1:3000/json', method: 'POST' },
{ name: 'GET /headers', url: 'http://127.0.0.1:3000/headers' },
]

const results: BenchmarkResult[] = []
Expand All @@ -111,6 +134,9 @@ async function runBenchmarkForServer(
if (bench.method === 'POST') {
args.push('-m', 'POST', '-H', 'Content-Type:application/json', '-f', './scripts/body.json')
}
if (bench.name === 'GET /headers') {
args.push('-H', 'x-test:123')
}
args.push(bench.url)

const output = await new Promise<string>((resolve, reject) => {
Expand Down Expand Up @@ -147,7 +173,8 @@ async function runBenchmarkForServer(
const ping = results[0]?.reqsPerSec || 0
const query = results[1]?.reqsPerSec || 0
const body = results[2]?.reqsPerSec || 0
const average = (ping + query + body) / 3
const headers = results[3]?.reqsPerSec || 0
const average = (ping + query + body + headers) / 4

return {
server: serverName,
Expand All @@ -156,14 +183,14 @@ async function runBenchmarkForServer(
ping,
query,
body,
headers,
}
} catch (error) {
console.error('Error:', (error as Error).message)
throw error
} finally {
console.log('Stopping server...')
server.kill()
await setTimeout(1000)
await stopServer(server)
}
}

Expand All @@ -185,14 +212,14 @@ async function testServer(serverFile: string, serverName: string): Promise<boole
console.log(' ', (error as Error)?.message || error)
return false
} finally {
server.kill()
await setTimeout(1000)
await stopServer(server)
}
}

async function main(): Promise<void> {
const servers = [
{ file: 'src/server-npm.js', name: '@hono/node-server (npm)' },
{ file: 'src/server-npm.js', name: '@hono/node-server (2.1.0)' },
{ file: 'src/server-srvx.js', name: 'srvx (0.12.5, fast)' },
{ file: 'src/server-dev.js', name: '@hono/node-server (dev)' },
]

Expand Down Expand Up @@ -238,47 +265,53 @@ async function main(): Promise<void> {
})
}

const formatDiff = (npm: number, dev: number): string => {
const diff = ((dev - npm) / npm) * 100
const sign = diff > 0 ? '+' : ''
return `${sign}${diff.toFixed(2)}%`
const formatDiff = (baseline: number, dev: number): string => {
const diff = ((dev - baseline) / baseline) * 100
return `${diff > 0 ? '+' : ''}${diff.toFixed(2)}%`
}

if (allResults.length === 2) {
// Comparison mode: npm vs dev
const npmResult = allResults.find((r) => r.server.includes('npm'))
if (allResults.length === 3) {
const npmResult = allResults.find((r) => r.server === '@hono/node-server (2.1.0)')
const srvxResult = allResults.find((r) => r.server === 'srvx (0.12.5, fast)')
const devResult = allResults.find((r) => r.server.includes('dev'))

if (npmResult && devResult) {
console.log('| Benchmark | npm | dev | Difference |')
console.log('| ----------------- | -------------- | -------------- | ----------- |')
if (npmResult && srvxResult && devResult) {
console.log(
'| Benchmark | @hono/node-server (2.1.0) | srvx (0.12.5, fast) | @hono/node-server (dev) | dev vs npm | dev vs srvx |'
)
console.log(
'| ----------------- | ------------------------- | ------------------- | ----------------------- | ---------- | ----------- |'
)
console.log(
`| Average | ${formatNumber(npmResult.average).padEnd(25)} | ${formatNumber(srvxResult.average).padEnd(19)} | ${formatNumber(devResult.average).padEnd(23)} | ${formatDiff(npmResult.average, devResult.average).padEnd(10)} | ${formatDiff(srvxResult.average, devResult.average).padEnd(11)} |`
)
console.log(
`| Average | ${formatNumber(npmResult.average).padEnd(14)} | ${formatNumber(devResult.average).padEnd(14)} | ${formatDiff(npmResult.average, devResult.average).padEnd(11)} |`
`| Ping (GET /) | ${formatNumber(npmResult.ping).padEnd(25)} | ${formatNumber(srvxResult.ping).padEnd(19)} | ${formatNumber(devResult.ping).padEnd(23)} | ${formatDiff(npmResult.ping, devResult.ping).padEnd(10)} | ${formatDiff(srvxResult.ping, devResult.ping).padEnd(11)} |`
)
console.log(
`| Ping (GET /) | ${formatNumber(npmResult.ping).padEnd(14)} | ${formatNumber(devResult.ping).padEnd(14)} | ${formatDiff(npmResult.ping, devResult.ping).padEnd(11)} |`
`| Query (GET /id) | ${formatNumber(npmResult.query).padEnd(25)} | ${formatNumber(srvxResult.query).padEnd(19)} | ${formatNumber(devResult.query).padEnd(23)} | ${formatDiff(npmResult.query, devResult.query).padEnd(10)} | ${formatDiff(srvxResult.query, devResult.query).padEnd(11)} |`
)
console.log(
`| Query (GET /id) | ${formatNumber(npmResult.query).padEnd(14)} | ${formatNumber(devResult.query).padEnd(14)} | ${formatDiff(npmResult.query, devResult.query).padEnd(11)} |`
`| Body (POST /json) | ${formatNumber(npmResult.body).padEnd(25)} | ${formatNumber(srvxResult.body).padEnd(19)} | ${formatNumber(devResult.body).padEnd(23)} | ${formatDiff(npmResult.body, devResult.body).padEnd(10)} | ${formatDiff(srvxResult.body, devResult.body).padEnd(11)} |`
)
console.log(
`| Body (POST /json) | ${formatNumber(npmResult.body).padEnd(14)} | ${formatNumber(devResult.body).padEnd(14)} | ${formatDiff(npmResult.body, devResult.body).padEnd(11)} |`
`| Headers (GET) | ${formatNumber(npmResult.headers).padEnd(25)} | ${formatNumber(srvxResult.headers).padEnd(19)} | ${formatNumber(devResult.headers).padEnd(23)} | ${formatDiff(npmResult.headers, devResult.headers).padEnd(10)} | ${formatDiff(srvxResult.headers, devResult.headers).padEnd(11)} |`
)
}
} else {
// Fallback: original table format
console.log(
'| Server | Runtime | Average | Ping | Query | Body |'
'| Server | Runtime | Average | Ping | Query | Body | Headers |'
)
console.log(
'| -------------------------- | ------- | ------------ | ------------ | ------------ | ------------ |'
'| -------------------------- | ------- | ------------ | ------------ | ------------ | ------------ | ------------ |'
)

const sortedResults = allResults.sort((a, b) => b.average - a.average)

for (const result of sortedResults) {
console.log(
`| ${result.server.padEnd(26)} | ${result.runtime.padEnd(7)} | ${formatNumber(result.average).padEnd(12)} | ${formatNumber(result.ping).padEnd(12)} | ${formatNumber(result.query).padEnd(12)} | ${formatNumber(result.body).padEnd(12)} |`
`| ${result.server.padEnd(26)} | ${result.runtime.padEnd(7)} | ${formatNumber(result.average).padEnd(12)} | ${formatNumber(result.ping).padEnd(12)} | ${formatNumber(result.query).padEnd(12)} | ${formatNumber(result.body).padEnd(12)} | ${formatNumber(result.headers).padEnd(12)} |`
)
}
}
Expand Down
2 changes: 2 additions & 0 deletions benchmarks/fetch/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export default {
switch (url.pathname) {
case '/':
return new Response('Hi')
case '/headers':
return new Response(request.headers.get('x-test'))
}

if (url.pathname.startsWith('/id/')) {
Expand Down
2 changes: 1 addition & 1 deletion benchmarks/fetch/src/server-dev.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { serve } from '@hono/node-server-dev'
import { serve } from '../../../dist/index.mjs'
import app from './app.js'

const port = 3000
Expand Down
12 changes: 12 additions & 0 deletions benchmarks/fetch/src/server-srvx.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { FastResponse, serve } from 'srvx'
import app from './app.js'

const port = 3000

// opt into srvx fast response, since hono uses request/response shims by default
globalThis.Response = FastResponse

serve({
fetch: app.fetch,
port,
})
33 changes: 15 additions & 18 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading