Skip to content

Hono + SSR + JWT

This example builds a small Hono API with three routes:

  1. GET /ping for health checks.
  2. POST /ssr to render a user card as HTML.
  3. POST /jwt to issue a valid signed JWT for a user.

This example is split into three files:

  1. hono_knitting.ts (the Hono server, plus a Knitting worker pool).
  2. hono_componets_ssr.tsx (SSR task: parse + defaults + render).
  3. hono_components_jwt.ts (JWT task: validate + sign + return JSON string).
  • Hono: a small, fast routing layer. It keeps the request path minimal so most overhead is in your actual route work.
  • @hono/node-server: a thin adapter that runs a Hono fetch handler on Node/Bun.
  • React SSR (react-dom/server): renders a tiny HTML page for /ssr so you can simulate CPU-heavy server work.
  • hono/jwt: signs a JWT (HS256) for /jwt so the example includes CPU work typical of authentication routes.
  • Knitting (knitting): runs selected transforms in a worker pool (threads). This keeps expensive route work off the request thread.

The JWT route uses hono/jwt and signs with HS256. Set JWT_SECRET in production.

This example imports TSX and npm packages. For Deno, keep a root deno.json like this:

{
"nodeModulesDir": "auto",
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "react"
}
}

Without this, TSX files can fail with:

Uncaught SyntaxError: Unexpected token '<'
bun.sh
JWT_SECRET="replace-me" bun src/hono_knitting.ts
Terminal window
# Ping
curl -s http://localhost:3000/ping
# SSR (returns HTML)
curl -s http://localhost:3000/ssr \
-H 'content-type: application/json' \
-d '{"name":"Ari","plan":"pro","bio":"Building on Knitting","projects":17}'
# JWT (returns JSON string with token)
curl -s http://localhost:3000/jwt \
-H 'content-type: application/json' \
-d '{"user":{"id":"u_42","email":"ari@example.com","role":"admin"},"ttlSec":900}'

Measurements: Hono 16-core benchmark.

One worker, with /ping, /ssr, and /jwt loaded concurrently.

RouteHono onlyHono + KnittingDelta
/ping2,815 rps8,486 rps+201%
/ssr2,815 rps4,816 rps+71%
/jwt2,578 rps4,712 rps+83%

One worker, with each route offered 2,000 rps.

RouteHono only p50Knitting p50Hono only p99Knitting p99
/ping0.81ms0.40ms16.93ms2.31ms
/ssr0.84ms2.22ms14.32ms8.71ms
/jwt1.03ms2.58ms25.18ms9.76ms

The latency improvement did not come from doing materially more CPU work. The host-only server used 1.08 server-process cores. The primary one-worker run used 1.10, and an equivalent repeat of the same one-worker configuration used 1.06.

Read those measurements as approximately 1.1 cores for both configurations, not as a meaningful difference between 1.08 and 1.10. The SSR and JWT computation does not disappear when it moves to the worker. Knitting keeps the transport, waiting, and coordination cost low enough that the same work costs about the same CPU while /ping no longer queues behind it.

These are workload-specific 15-second runs. The full report documents the methodology, repeat variance, and limitations; they are evidence for the design, not a universal performance guarantee.

  • ping stays cheap and synchronous.
  • You can benchmark workers vs host-only with the same route behavior.
  • In Knitting mode, the JWT task returns stringified JSON to reduce structured-clone overhead.
  • Heavy route logic stays in one shared file, keeping both server entrypoints small.
hono_knitting.ts
import { serve } from "@hono/node-server";
import { createPool } from "knitting";
import { Hono } from "hono";
import { issueJwt } from "./hono_components_jwt.ts";
import { renderSsrPage } from "./hono_componets_ssr.tsx";
const handlers = createPool({
})({
issueJwt,
renderSsrPage,
});
async function main() {
const app = new Hono();
app.get("/ping", (c) => {
return c.json({
ok: true,
pong: true,
runtime: process.release?.name ?? "unknown",
ts: new Date().toISOString(),
});
});
app.post("/ssr", async (c) => {
const html = await handlers.call.renderSsrPage(c.req.arrayBuffer());
return c.html(html);
});
app.post("/jwt", async (c) => {
const responseJson = await handlers.call.issueJwt(c.req.arrayBuffer());
return c.body(responseJson ?? "Bad request", responseJson ? 200 : 400, {
"content-type": "application/json; charset=utf-8",
});
});
const server = serve({ fetch: app.fetch, port: 3000 }, (info) => {
console.log("GET /ping");
console.log("POST /ssr body: { name?, plan?, bio?, projects? }");
console.log("POST /jwt body: { user: { id, email?, role? }, ttlSec? }");
});
const close = () => {
// IMPORTANT TO CLOSE CONNECTION
handlers.shutdown();
server.close();
};
process.on("SIGINT", close);
process.on("SIGTERM", close);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});