Edge APIs with Cloudflare Workers and Hono: A Production Playbook
I've shipped APIs on Node, Express, Fastify, and a dozen serverless platforms. Nothing has matched the developer experience and raw performance I've gotten since I started building on Cloudflare Workers with the Hono framework. In this playbook, I'll walk you through the exact patterns I use to ship production-grade edge APIs that boot in milliseconds, scale to global traffic, and stay type-safe from request to response.
If you're evaluating edge computing for your next API, or if you've been burned by cold starts and regional latency, this guide is for you.
Why Cloudflare Workers and Hono Are a Perfect Match
Cloudflare Workers run your code in V8 isolates at 300+ data centers worldwide. There are no containers to spin up, no cold starts in the traditional sense, and requests are routed to the nearest POP. The runtime is a subset of the standard Web APIs, which keeps bundles small and execution fast.
Hono is a small, ultrafast web framework built specifically for this kind of runtime. It uses the Web Standards Request and Response objects, supports TypeScript-first routing, and weighs just a few kilobytes. The two fit together like they were designed for each other — because Hono was.
Here's what you get out of the box:
- Zero cold start — isolates are kept warm, so the first request isn't penalized
- Sub-millisecond routing — Hono's regex-based router is one of the fastest in the JavaScript ecosystem
- Type safety end to end — middleware, validators, and RPC clients all share your Zod or TypeScript types
- Global by default — your API runs everywhere Cloudflare runs
Project Setup: From Zero to a Running Worker
Let's scaffold a project. I prefer wrangler for local development because it mirrors production behavior closely.
npm create hono@latest my-edge-api
cd my-edge-api
npm install
When prompted, pick the Cloudflare Workers template. Hono's CLI generates a minimal setup with wrangler.toml, src/index.ts, and a package.json wired up with the latest dependencies.
Your wrangler.toml should look like this:
name = "my-edge-api"
main = "src/index.ts"
compatibility_date = "2024-09-01"
[[kv_namespaces]]
binding = "CACHE"
id = "your-kv-namespace-id"
I bind my Workers KV namespaces here so the runtime can inject them as environment variables. I'll cover binding management further down.
Type-Safe Routing with Hono
The first thing I love about Hono is how clean the routing feels. Here's a real example from one of my production APIs — a URL shortener:
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
type Bindings = {
CACHE: KVNamespace
LINKS: DurableObjectNamespace
}
const app = new Hono<{ Bindings: Bindings }>()
const CreateLinkSchema = z.object({
url: z.string().url(),
slug: z.string().min(3).max(32).regex(/^[a-z0-9-]+$/),
})
app.post('/links', zValidator('json', CreateLinkSchema), async (c) => {
const { url, slug } = c.req.valid('json')
const stub = c.env.LINKS.get(c.env.LINKS.idFromName(slug))
await stub.fetch('https://internal/create', {
method: 'POST',
body: JSON.stringify({ url, slug }),
})
await c.env.CACHE.put(slug, url, { expirationTtl: 60 * 60 * 24 })
return c.json({ slug, url }, 201)
})
app.get('/:slug', async (c) => {
const slug = c.req.param('slug')
const cached = await c.env.CACHE.get(slug)
if (cached) return c.redirect(cached, 302)
const stub = c.env.LINKS.get(c.env.LINKS.idFromName(slug))
const res = await stub.fetch(`https://internal/resolve/${slug}`)
if (!res.ok) return c.notFound()
const { url } = await res.json()
return c.redirect(url, 302)
})
export default app
Notice how the Bindings type flows through the entire request lifecycle. When I call c.env.CACHE, TypeScript knows it's a KVNamespace. When I call c.env.LINKS, it knows it's a DurableObjectNamespace. No any, no casting, no surprises.
Using Workers KV for Edge Caching
Workers KV is Cloudflare's eventually-consistent key-value store. It's perfect for caching API responses, feature flags, configuration, and short-lived data. I treat it as a read-through cache with a sane TTL.
A pattern I reach for constantly:
async function cachedFetch(
c: Context, key: string, ttl: number, fetcher: () => Promise<any>
) {
const hit = await c.env.CACHE.get(key, 'json')
if (hit) return c.json(hit)
const fresh = await fetcher()
await c.env.CACHE.put(key, JSON.stringify(fresh), { expirationTtl: ttl })
return c.json(fresh)
}
The rule of thumb I follow: read-heavy, write-occasional data lives in KV. For strong consistency or high write rates, I use a Durable Object instead. KV is replicated globally but writes can take up to 60 seconds to propagate — that tradeoff is usually fine for cache-like data.
Durable Objects for Stateful Logic
Durable Objects give you a single point of coordination per key with strong consistency. They're the right tool for rate limiting, real-time counters, websockets, and small but important state machines. In my link shortener example, the Durable Object owns the canonical mapping for a slug:
export class LinkObject implements DurableObject {
state: DurableObjectState
url: string | null = null
constructor(state: DurableObjectState) {
this.state = state
this.state.blockConcurrencyWhile(async () => {
this.url = (await this.state.storage.get<string>('url')) ?? null
})
}
async fetch(request: Request) {
const url = new URL(request.url)
if (url.pathname === '/create' && request.method === 'POST') {
const body = await request.json<{ url: string }>()
this.url = body.url
await this.state.storage.put('url', this.url)
return new Response('ok')
}
if (url.pathname.startsWith('/resolve/')) {
return this.url
? new Response(JSON.stringify({ url: this.url }), {
headers: { 'content-type': 'application/json' },
})
: new Response('not found', { status: 404 })
}
return new Response('not found', { status: 404 })
}
}
The blockConcurrencyWhile call is critical — it ensures we hydrate state from storage before serving any requests, eliminating a class of race conditions I've debugged too many times in production.
Authentication at the Edge
JWT verification is one of my favorite workloads on Workers because there's no network call to a third party. I sign tokens with HS256, then verify them in a middleware:
import { jwt } from 'hono/jwt'
app.use('/api/*', jwt({
secret: c => c.env.JWT_SECRET,
alg: 'HS256',
}))
app.get('/api/me', (c) => c.json({ user: c.get('jwtPayload') }))
For OAuth flows, I use Hono's bearerAuth or write a small custom middleware that validates against a session stored in KV. The point is: auth happens at the edge, before a single byte reaches your origin.
Performance: Tips from the Trenches
After running production edge APIs for two years, here are the optimizations that actually matter:
- Keep your bundle small. Every kilobyte matters when it runs on every request. I tree-shake aggressively and avoid heavy polyfills.
esbuild's minifier + Workers' tree-shaking gets most apps under 50 KB. - Use
c.executionCtx.waitUntil()for background work. Don't block the response on logging, analytics, or cache warmups — fire them inwaitUntilso the user gets their response immediately. - Cache at every layer. The Cloudflare CDN sits in front of your Worker. Combine that with Workers KV, and you can serve most reads from a single POP with zero compute.
- Stream responses. For large payloads, use
return c.body(stream)instead of building a string in memory. Workers can stream from R2 directly without buffering. - Profile with
wrangler tail. Real-time logs show you exactly where time is spent, including subrequest latencies. It's the single most useful debugging tool I have.
Deployment and Observability
Deployment is one command:
npx wrangler deploy
For staging environments, I keep separate wrangler.toml files and use environments:
[env.staging]
name = "my-edge-api-staging"
[[env.staging.kv_namespaces]]
binding = "CACHE"
id = "staging-kv-id"
For observability, I lean on Cloudflare Analytics for traffic patterns, Logpush for structured logs, and Sentry for error tracking. The Workers logs dashboard also has Tail Workers, which let you process every log line in code — perfect for sampling, redacting, or shipping to a third party.
Common Pitfalls to Avoid
I've made these mistakes so you don't have to:
- Don't use Node-only APIs. Cloudflare's runtime is V8 isolates, not Node. No
fs, nocrypto.createHashwith old algorithms, noBufferwithout a polyfill. Stick to Web Standards. - Don't store secrets in code. Use
wrangler secret put MY_SECRETso they're encrypted at rest. - Don't ignore CPU time limits. The free plan gives you 10ms; the paid plan gives you 50ms for most operations, more for scheduled work. If you need heavy compute, offload it to a queue.
- Don't skip
blockConcurrencyWhileon Durable Objects. I called this out above, but it's the #1 source of subtle bugs in DO code.
Wrapping Up
Building edge APIs on Cloudflare Workers with Hono is one of the most productive developer experiences I've had in over six years of writing backend code. You get global low-latency by default, a tiny and fast framework, and a type system that catches mistakes before they hit production. The patterns above — KV caching, Durable Objects for state, JWT middleware, streaming responses — are the ones I reach for on almost every project.
If you're starting a new API project, give this stack a serious look. And if you want help designing or scaling a production edge API, let's talk — I love this stuff.
FAQ
What is Hono and why use it with Cloudflare Workers?
Hono is a small, ultrafast web framework built for edge runtimes. It uses Web Standard Request/Response objects, supports TypeScript-first routing, and weighs only a few kilobytes. It runs natively on Cloudflare Workers, Bun, Deno, and Node, making it ideal for building edge APIs with strong type safety and excellent performance.
How do Workers KV and Durable Objects differ?
Workers KV is a globally replicated, eventually-consistent key-value store best for read-heavy workloads like caching, configuration, and feature flags. Durable Objects are strongly consistent, single-instance per key, and best for stateful logic like rate limiting, counters, and WebSocket coordination. Use KV for cache-like data and Durable Objects when you need coordination or strong consistency.
Can I use Hono with TypeScript for full type safety?
Yes. Hono is built with TypeScript and integrates with @hono/zod-validator, hono/client for end-to-end typed RPC, and type-safe bindings. You can share types between your client and server, eliminating entire classes of bugs around request/response shape mismatches.
What are the cost implications of running APIs on Cloudflare Workers?
Cloudflare Workers includes 100,000 free requests per day. The paid plan is $5/month for 10 million requests plus bandwidth and CPU time. For most APIs serving under 10 million requests, your total Cloudflare bill will be under $10–$25/month — significantly cheaper than most container or VM-based alternatives.
How do I debug and monitor a Hono app on Cloudflare Workers?
Use wrangler dev for local development, wrangler tail for real-time production logs, and Cloudflare Analytics for traffic and error metrics. For error tracking, integrate Sentry via the official Workers SDK. Tail Workers also let you process every log line in code for custom observability pipelines.