Edge Functions Cheat Sheet
Writing and deploying edge functions across major platforms (Vercel, Cloudflare Workers, Deno Deploy) using the Web standard APIs.
Vercel Edge Function
Runs on the V8 isolate edge runtime instead of Node, close to the user.
// app/api/geo/route.tsexport const runtime = 'edge'export async function GET(req: Request) { const geo = req.headers.get('x-vercel-ip-country') ?? 'unknown' return new Response(JSON.stringify({ country: geo }), { headers: { 'content-type': 'application/json' }, })}
Cloudflare Worker
Standard fetch-handler module syntax, deployed globally via Wrangler.
export default { async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> { const url = new URL(request.url) if (url.pathname === '/kv-test') { const value = await env.MY_KV.get('key1') return new Response(value ?? 'not found') } return new Response('Hello from the edge!') },}// wrangler.toml// name = "my-worker"// main = "src/index.ts"// compatibility_date = "2026-01-01"// [[kv_namespaces]]// binding = "MY_KV"// id = "..."
Deno Deploy Function
Deploy a standard Deno.serve handler globally with zero config.
Deno.serve(async (req: Request) => { const { pathname } = new URL(req.url) if (pathname === '/health') { return Response.json({ status: 'ok', region: Deno.env.get('DENO_REGION') }) } return new Response('Not Found', { status: 404 })})// deploy: deployctl deploy --project=my-app main.ts
Edge Middleware (Auth Gate)
A common edge pattern: intercept requests before they hit origin to redirect/rewrite.
// middleware.ts (Next.js, runs on the edge by default)import { NextResponse } from 'next/server'import type { NextRequest } from 'next/server'export function middleware(req: NextRequest) { const token = req.cookies.get('session')?.value if (!token && req.nextUrl.pathname.startsWith('/dashboard')) { return NextResponse.redirect(new URL('/login', req.url)) } return NextResponse.next()}export const config = { matcher: '/dashboard/:path*' }
Edge Runtime Constraints
What's different vs. a normal Node server.
- No native Node APIs- fs, net, child_process are unavailable; use Web Standard fetch/Request/Response instead
- Cold start- typically sub-50ms since it's a V8 isolate, not a container spin-up
- CPU/time limits- Cloudflare Workers free tier caps at 10ms CPU time (50ms paid); Vercel Edge has its own execution limits
- No persistent in-memory state- each request may hit a different isolate; use KV/Durable Objects/external DB for state
- Bundle size limits- Workers cap script size (1MB compressed on paid plans); tree-shake aggressively
Cloudflare Durable Objects for Edge State
A Durable Object gives a single-threaded, strongly consistent instance per ID — the standard way to hold state (counters, WebSocket rooms) at the edge.
export class Counter { state: DurableObjectState constructor(state: DurableObjectState) { this.state = state } async fetch(request: Request): Promise<Response> { let count = (await this.state.storage.get<number>('count')) ?? 0 count++ await this.state.storage.put('count', count) return new Response(String(count)) }}// worker entryexport default { async fetch(req: Request, env: Env) { const id = env.COUNTER.idFromName('global') const stub = env.COUNTER.get(id) return stub.fetch(req) },}
Streaming a Response at the Edge
Return a ReadableStream so the client starts receiving bytes before the whole payload is generated, e.g. for LLM token streaming.
export const runtime = 'edge'export async function GET() { const encoder = new TextEncoder() const stream = new ReadableStream({ async start(controller) { for (const chunk of ['Hello', ', ', 'world', '!']) { controller.enqueue(encoder.encode(chunk)) await new Promise((r) => setTimeout(r, 100)) } controller.close() }, }) return new Response(stream, { headers: { 'content-type': 'text/plain' } })}
Cache API for Edge Response Caching
The standard Web Cache API lets an edge function cache expensive responses per-region without an external store.
export default { async fetch(request: Request, env: Env, ctx: ExecutionContext) { const cache = caches.default let response = await cache.match(request) if (response) return response response = await fetch('https://api.example.com/expensive') response = new Response(response.body, response) response.headers.append('Cache-Control', 's-maxage=60') ctx.waitUntil(cache.put(request, response.clone())) return response },}
waitUntil for Background Work
Finish work (logging, cache writes, analytics) after the response has already been sent to the client, without blocking latency.
export default { async fetch(request: Request, env: Env, ctx: ExecutionContext) { const response = new Response('OK') ctx.waitUntil( fetch('https://analytics.example.com/log', { method: 'POST', body: JSON.stringify({ path: new URL(request.url).pathname }), }) ) return response // client gets this immediately; the fetch above still completes },}
Platform Trade-offs Beyond the Basics
Details that matter once you're choosing between platforms for a real workload, not a demo.
- Vercel Edge Middleware vs Edge Functions- middleware runs before routing on every matched request; functions are full route handlers
- Cloudflare Durable Objects- the main way to get strongly consistent state at the edge; Vercel/Deno Deploy lean on external KV/DB instead
- Regional invocation vs true multi-region- some 'edge' functions actually run in a handful of regions, not literally every PoP — check the platform's docs before assuming global sub-50ms
- wrangler dev --local vs --remote- local simulates the edge runtime; remote actually executes against Cloudflare's network for closer-to-prod testing
- Node API polyfill gaps- Cloudflare's `nodejs_compat` flag covers much of node: but not all — verify before porting existing Node middleware
Don't assume edge = faster for everything — if your function does a single round trip to a centralized regional database, you've just added a network hop before the real latency; edge wins when the data (KV, Durable Objects, cache) is also distributed to the edge.