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
- Modern TypeScript Backend with Hono on Bun
- Cloudflare Workers + D1 + Durable Objects
- Cloudflare vs AWS vs Vercel for Backend in 2026
- Drizzle ORM Deep Dive
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 .