When building HTTP APIs with Spin, performance matters—not just in raw response time, but also in startup latency and memory footprint, especially for WebAssembly modules running in constrained environments.
In this post, we will break down the performance characteristics of three approaches to routing in Spin applications written in JavaScript or TypeScript:
- Using the Hono Router
- Using the Itty Router
- Using no router at all, writing direct logic in Spin’s handler
Why Routing Performance Matters in Spin
Spin apps are built for speed, fast startup, tiny binaries, and low-latency execution. However, when every millisecond counts, even your choice of router can become a bottleneck. Adding unnecessary abstraction could impact the overall application performance. That’s why it’s critical to understand the tradeoffs between routing libraries and raw routing logic.
The Setup
We created a minimal HTTP API that must be able to respond to the following request characteristics:
GET /items
: To return a list of items as JSONGET /items/:id
: To return a particular item as JSON looked up by its identifier
We created three distinct applications with Spin v3.2.0
and latest http-js
templates. For benchmarking with hey
those apps are hosted locally using spin up
.
The Hono Implementation
import { Hono } from 'hono/quick'
let items = [
{ id: 1, name: 'coffee' },
{ id: 2, name: 'soda' },
{ id: 3, name: 'milk' },
]
let app = new Hono()
app.get('/items', (c) => c.json(items))
app.get('/items/:id', (c) => {
let id = +c.req.param('id')
let found = items.find(i => i.id === id)
if (!found) {
return c.status(404)
}
return c.json(found)
})
app.fire()
The Itty Implementation
import { AutoRouter, json, status } from 'itty-router'
let items = [
{ id: 1, name: 'coffee' },
{ id: 2, name: 'soda' },
{ id: 3, name: 'milk' },
]
let router = AutoRouter()
router.get('/items', () => {
return json(items)
}).get('/items/:id', ({ id }) => {
let found = items.find(i => i.id === +id)
if (!found) {
return status(404)
}
return json(found)
})
addEventListener('fetch', (event) => {
event.respondWith(router.fetch(event.request))
})
The Manual Routing Implementation
let items = [
{ id: 1, name: 'coffee' },
{ id: 2, name: 'soda' },
{ id: 3, name: 'milk' },
]
function handle(request) {
const url = new URL(request.url)
const path = url.pathname.slice(1)
const method = request.method
if (method === 'GET' && (path === 'items' || path === 'items/')) {
return new Response(JSON.stringify(items), {
status: 200,
headers: {
'content-type': 'application/json'
}
})
}
const id = path.split('/')[1]
if (method === 'GET' && !!id) {
try {
const item = items.find(i => i.id === parseInt(id, 10))
if (item) {
return new Response(JSON.stringify(item), {
status: 200,
headers: {
'content-type': 'application/json'
}
});
}
} catch (err) {
return new Response(null, { status: 400 })
}
}
return new Response(null, { status: 404 })
}
addEventListener('fetch', (event) => {
event.respondWith(handle(event.request))
})
Benchmarking
We used hey to stress test each routing setup with 200
concurrent workers. Each configuration was tested under constant load for 5s
, 10s
, and 20s
, repeating each test 5
times. From these runs, we calculated the average requests per second (RPS) and latency percentiles (95th and 99th).
All tests were performed on an Azure Standard D8s-v5 VM, equipped with 8 vCPUs and 32 GiB of RAM, running Debian 12 Bookworm.
Router | Average RPS | 95th | 99th |
---|---|---|---|
Manual Routing | 4922.2 | 15.7ms | 17.5ms |
Itty Router | 3718.3 | 20.1ms | 23.5ms |
Hono Router | 3509.8 | 21.4ms | 24.3ms |
Analysis
Manual Routing
Manual routing delivered the highest throughput at 4922 RPS
and the lowest tail latencies—15.7ms
at the 95th percentile, 17.5ms
at the 99th. This approach avoids all abstraction overhead and is ideal for tight, performance-critical APIs.
👉 If you’re chasing even lower latencies and higher throughput, consider writing Spin components in Rust. With Rust’s zero-cost abstractions and native WebAssembly performance, you can push Spin performance even further.
Itty Router
Itty Router is just behind manual routing at 3718 RPS
, with 95th / 99th percentile latencies of 20.1ms
/ 23.5ms
. The drop in speed is noticeable but still performant enough for many real-world use cases. If you want clean route definitions without a significant performance hit, Itty is a solid pick.
Hono Router
Hono comes in last in terms of speed, with 3509 RPS
and slightly higher latencies—21.4ms
(p95) and 24.3ms
(p99). While not slow by any stretch, it does add measurable overhead especially when compared to the manual routing approach. That tradeoff may be worth it if you need Hono’s features like middleware, context management, or advanced routing patterns.
When to Use What?
Scenario | Recommended Approach |
---|---|
You need maximum performance | Rust |
You’re building a simple, focused API | Manual Routing |
You want structured routing with low overhead | Itty Router |
Your API requires advanced routing features or middleware | Hono Router |
Each option has tradeoffs in terms of performance, complexity, and additional dependencies. Choose based on your application’s routing needs and performance budget — not just familiarity with a library.
Final Thoughts
Spin is designed for speed, but the layers you build on top determine how much of that speed you actually keep. Manual routing in JavaScript and/or TypeScript offers the best performance within the JS ecosystem, but if you’re after maximum efficiency, Rust remains the gold standard. Its zero-cost abstractions and native-level WebAssembly performance outperform even the leanest JavaScript setups.
That said, tools like Itty Router offer a solid middle ground with minimal overhead, and Hono gives you expressive power when your API needs it. The key is matching your tooling to your app’s complexity and performance goals.