Hono on Cloudflare Workers is the cheapest, fastest backend stack in 2026 — for the workloads that fit. This post is the production patterns from real edge-deployed apps.

A real backend

// src/index.ts
import { Hono } from "hono";
import { cors } from "hono/cors";
import { logger } from "hono/logger";
import { drizzle } from "drizzle-orm/d1";
import { eq } from "drizzle-orm";
import { users } from "./schema";

type Env = {
  DB: D1Database;
  R2: R2Bucket;
  KV: KVNamespace;
  ROOM: DurableObjectNamespace;
  AI: Ai;
};

const app = new Hono<{ Bindings: Env }>();

app.use("*", logger());
app.use("*", cors({ origin: ["https://app.example.com"] }));

app.get("/health", (c) => c.json({ ok: true }));

app.get("/users/:id", async (c) => {
  const db = drizzle(c.env.DB);
  const id = Number(c.req.param("id"));
  const user = await db.select().from(users).where(eq(users.id, id)).get();
  if (!user) return c.json({ error: "not_found" }, 404);
  return c.json(user);
});

export default app;
export { ChatRoom } from "./room";        // Durable Object class

Compact. Type-safe end-to-end. Deploys with wrangler deploy.

For broader stack see Modern TypeScript Backend with Hono on Bun .

Auth

Bearer-token style:

import { jwt } from "hono/jwt";

app.use("/api/*", jwt({ secret: c.env.JWT_SECRET }));

app.get("/api/me", (c) => {
  const payload = c.get("jwtPayload");
  return c.json({ user_id: payload.sub });
});

For passkeys / OAuth see Authentication in 2026 . Same patterns; verified at the edge.

Postgres via Hyperdrive

Workers can’t open many Postgres connections (each isolate is short-lived). Hyperdrive pools at the edge:

import postgres from "postgres";

const sql = postgres(c.env.HYPERDRIVE.connectionString);
const users = await sql`SELECT * FROM users WHERE id = ${id}`;

Hyperdrive maintains a pool to your origin Postgres. Workers get cheap connections.

Durable Objects

For per-tenant or per-room state:

export class TenantState {
  state: DurableObjectState;
  
  async fetch(req: Request) {
    const url = new URL(req.url);
    if (url.pathname === "/messages") {
      const messages = await this.state.storage.sql.exec(
        "SELECT * FROM messages ORDER BY ts DESC LIMIT 100"
      ).toArray();
      return Response.json(messages);
    }
    // ...
  }
}

DOs have native SQLite storage (since 2024). Strongly consistent. Co-located with code. See Cloudflare Workers + D1 + Durable Objects .

Errors

app.onError((err, c) => {
  if (err instanceof HTTPException) {
    return err.getResponse();
  }
  console.error(err);
  return c.json({ error: "internal" }, 500);
});

HTTPException from hono/http-exception for known errors; default for unknown. Logs ship to Cloudflare logs (or external via OTel ).

Observability

import { opentelemetry } from "@hono/otel";

app.use("*", opentelemetry({
  exporter: new OTLPTraceExporter({ url: env.OTEL_ENDPOINT }),
  serviceName: "my-api",
}));

OpenTelemetry traces per request. Forwards to Tempo / Honeycomb / Datadog.

Workers AI for cheap inference

const embedding = await c.env.AI.run("@cf/baai/bge-base-en-v1.5", {
  text: "hello",
});

Edge embeddings, ~$0.01 per 1k tokens. For RAG over modest corpora paired with Vectorize: complete pipeline at edge cost. See Embedding Models in 2026 .

R2 for blob storage

app.put("/files/:name", async (c) => {
  const name = c.req.param("name");
  await c.env.R2.put(name, c.req.raw.body);
  return c.json({ ok: true });
});

app.get("/files/:name", async (c) => {
  const obj = await c.env.R2.get(c.req.param("name"));
  if (!obj) return c.text("not found", 404);
  return new Response(obj.body, { headers: obj.httpMetadata });
});

S3-compatible. Free egress (vs AWS’s $0.09/GB).

Streaming

import { streamSSE } from "hono/streaming";

app.post("/chat", (c) => {
  return streamSSE(c, async (stream) => {
    const llm = anthropic.messages.stream({ ... });
    for await (const ev of llm) {
      if (ev.type === "content_block_delta") {
        await stream.writeSSE({ data: JSON.stringify({ text: ev.delta.text }) });
      }
    }
  });
});

Workers handle SSE natively. Pair with an LLM stream for chat. See SSE vs WebSockets in 2026 .

Deployment

# wrangler.toml
name = "my-api"
main = "src/index.ts"
compatibility_date = "2026-04-01"

[[d1_databases]]
binding = "DB"
database_name = "my-app"
database_id = "..."

[[r2_buckets]]
binding = "R2"
bucket_name = "my-app-files"

[[durable_objects.bindings]]
name = "TENANT"
class_name = "TenantState"

[[hyperdrive]]
binding = "HYPERDRIVE"
id = "..."

[ai]
binding = "AI"

wrangler deploy. ~30 seconds globally.

Performance

For typical CRUD endpoints:

  • TTFB: 30–80ms globally.
  • p99 latency: <200ms.
  • CPU per request: 5–20ms (well under Workers limits).
  • Memory: 30–80 MB (within 128 MB limit).

For Workers at scale:

  • 100M req/month: ~$30.
  • 1B req/month: ~$200.

Compare AWS Lambda: ~3-5× cost for same. See Cloudflare vs AWS vs Vercel .

Common mistakes

1. Treating Workers like containers

Each Worker invocation is short-lived. No long connections, no in-memory state across requests. Use D1 / KV / DO for persistence.

2. Blowing CPU limit

50ms (free), 30s (paid). Long-running CPU tasks don’t fit. Move to Cloudflare Containers (newer) or external compute.

3. Cold starts on critical paths

Workers cold starts are <10ms. Far cheaper than Lambda. But test perceived latency.

4. Ignoring binding declarations

Bindings (DB, KV, R2) are declared in wrangler.toml. Forget one; runtime error. Type generation via wrangler types.

5. Not testing locally

wrangler dev runs Workers locally with miniflare. Tests must verify edge semantics, not just app logic.

Read this next

If you want my Hono on Workers production starter, it’s at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .