Spin JS/TS Router Showdown: Hono vs. Itty vs. Manual Routing

Thorsten Hans headshot

Apr 30, 2025

Thorsten Hans

Thorsten Hans headshot

Written by

Thorsten Hans

Thorsten Hans is a Senior Developer Advocate at Akamai. He guides developers and teams through understanding, adopting, and mastering emerging technologies to build reliable software and embrace the next wave of cloud computing. As Docker Captain, he continues to share his experiments and knowledge with the developer community.

Share

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, and 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 trade-offs 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: Return a list of items as JSON
  • GET /items/:id: Return a particular item as JSON looked up by its identifier

We created three distinct applications with Spin v3.2.0 and the 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

3718.3

20.1ms

23.5ms

Hono

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

Itty 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

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 trade-off 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

Your API requires advanced routing features or middleware

Hono

Each option has trade-offs 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 allow it to outperform even the leanest JavaScript setups.

That said, tools like Itty 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.

Thorsten Hans headshot

Apr 30, 2025

Thorsten Hans

Thorsten Hans headshot

Written by

Thorsten Hans

Thorsten Hans is a Senior Developer Advocate at Akamai. He guides developers and teams through understanding, adopting, and mastering emerging technologies to build reliable software and embrace the next wave of cloud computing. As Docker Captain, he continues to share his experiments and knowledge with the developer community.

Tags

Share

Related Blog Posts

Cloud
No Lag, All Frag: Level Up Your Gaming with Xonotic, K3s, and Edge Computing
March 20, 2025
Let’s set the scene for a gamer: you’re having the game of your life (and you wish you were streaming today of all days). You’re lining up the perfect Level up your gaming with Xonotic, K3s, and edge computing! Discover how to host a high-performance, low-latency game server using Akamai Cloud’s distributed compute regions. Say goodbye to lag and hello to seamless gameplay.
Cloud
An Inside Look at our Next Gen Object Storage Launch
August 28, 2025
Cloud
How To Lower Your Live Video Transcoding Costs
August 15, 2024
If you're an engineer in charge of live streaming, you're likely facing the task of managing live video transcoding for millions of users.