TL;DR
- Edge functions run your code in hundreds of locations on V8 isolates, so cold starts are a few milliseconds.
- You get Web standard APIs (
Request,Response,fetch,crypto.subtle), not Node.js. Check your dependencies. - Great for auth checks, API-key proxying, A/B tests, redirects, and caching.
- Terrible when your database is in one region and your handler makes several round trips to it.
- Use edge and regional functions together. They're not rivals.
Most explanations of edge functions stop at "your code runs closer to the user." True, and also the least useful thing to know when deciding whether to ship one. Here's the practical version, with code you can paste into a Cloudflare Worker. Vercel, Deno Deploy, Netlify, and Supabase edge functions need only small changes.
What's actually different
A regional server or serverless function lives in one place, say ap-south-1. A user in Sydney pays a 200 to 300 ms round trip to Mumbai before your code runs a single line. An edge function is deployed everywhere at once and answers the Sydney user from Sydney.
The other difference is the runtime. Regular serverless boots a container or micro-VM per function, which is where cold starts come from. Edge platforms mostly use V8 isolates, the same sandbox that separates Chrome tabs. Thousands share one process and a new one starts in milliseconds.
The catch: no Node.js. Anything that imports fs or a native module is out. Node compat layers exist but they're partial.
Hello, edge
export default {
async fetch(request, env, ctx) {
const country = request.cf?.country ?? "unknown";
return Response.json({ message: "Hello from the edge", country });
},
};
Take a Request, return a Response. That's the whole contract. request.cf is Cloudflare-specific; other platforms expose geo data via headers.
Edge vs serverless, in one table
| Serverless | Edge | |
|---|---|---|
| Location | One region | Everywhere |
| Isolation | Container / micro-VM | V8 isolate |
| Cold start | 100s of ms | A few ms |
| Runtime | Full Node, Python, Go | Web APIs, partial Node |
| Duration | Minutes | Seconds of CPU |
| Best for | Heavy compute, long jobs | Short, latency-sensitive logic |
Pattern 1: verify auth before the origin sees the request
import { jwtVerify } from "jose";
export default {
async fetch(request, env) {
const token = (request.headers.get("Authorization") ?? "").replace("Bearer ", "");
if (!token) return new Response("Unauthorized", { status: 401 });
try {
const secret = new TextEncoder().encode(env.JWT_SECRET);
const { payload } = await jwtVerify(token, secret);
const forwarded = new Request(request);
forwarded.headers.set("x-user-id", payload.sub);
return fetch(forwarded);
} catch {
return new Response("Invalid token", { status: 401 });
}
},
};
Use jose, not jsonwebtoken. The latter needs Node's crypto module and dies in edge runtimes. This is the first wall most people hit.
Pattern 2: proxy third-party APIs so your keys never ship
Any key inside a mobile or web bundle can be extracted. Hide it behind a Worker:
export default {
async fetch(request, env) {
if (request.method !== "POST") {
return new Response("Method not allowed", { status: 405 });
}
const { prompt } = await request.json();
const upstream = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${env.OPENAI_API_KEY}`,
},
body: JSON.stringify({
model: "gpt-6-luna",
messages: [{ role: "user", content: prompt }],
}),
});
return new Response(upstream.body, {
status: upstream.status,
headers: { "Content-Type": "application/json" },
});
},
};
The client only sees your endpoint:
const res = await fetch("https://api.example.com/ai", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt: "Summarise my notes" }),
});
This matters most for React Native and Expo apps, which have no server by default. If you generate your app with RapidNative, put a proxy like this in front of every paid API before you ship. And add per-user rate limiting, because a hidden key behind an open endpoint is still an open wallet.
Pattern 3: A/B tests without the flicker
Client-side experiments render, then swap content. Users see the flash. Decide at the edge instead:
export default {
async fetch(request) {
const cookie = request.headers.get("Cookie") ?? "";
let variant = cookie.match(/variant=(a|b)/)?.[1];
const isNew = !variant;
if (isNew) variant = Math.random() < 0.5 ? "a" : "b";
const url = new URL(request.url);
url.pathname = `/${variant}${url.pathname}`;
const response = await fetch(new Request(url, request));
const result = new Response(response.body, response);
if (isNew) {
result.headers.append("Set-Cookie", `variant=${variant}; Path=/; Max-Age=2592000`);
}
return result;
},
};
Pattern 4: cache responses near the user
export default {
async fetch(request, env, ctx) {
const cache = caches.default;
let response = await cache.match(request);
if (response) return response;
response = await fetch(request);
response = new Response(response.body, response);
response.headers.set("Cache-Control", "public, max-age=60");
ctx.waitUntil(cache.put(request, response.clone()));
return response;
},
};
ctx.waitUntil lets the cache write finish after the response is sent, so the user never waits on it.
The mistake that makes edge functions slower
Your function runs in Sydney. Your Postgres runs in Virginia. Every query crosses the Pacific.
Three sequential queries in a handler means three long round trips. A boring regional function next to the database would have won.
Ways out:
- Cache reads at the edge for anything that tolerates being a bit stale.
- Use a distributed store (edge KV, replicated SQLite, Postgres read replicas).
- Use an HTTP driver or connection pooler. Isolates can't hold long-lived TCP pools, and you'll exhaust connections fast without one.
- Pin the function to a region for write-heavy endpoints. Most platforms let you.
Rule of thumb: more than one uncached DB round trip per handler? Measure before assuming the edge is faster.
Other limits to plan for
- CPU time: image processing, PDF generation, and big JSON transforms hit the ceiling.
- Bundle size: fat dependencies may not fit.
- Package compat: check edge support before designing around a library.
- Observability: code in hundreds of locations needs structured logs and a request id on every line.
When to reach for them
Short logic, runs on most requests, latency matters, no far-away database dependency. Auth, redirects, geo, personalisation, proxying, webhooks, caching.
Everything else (heavy compute, long jobs, native deps, transactional DB work) goes to a regional function or a server. Most real systems run both.
What are you running at the edge right now? Drop it in the comments, especially if it bit you.
Top comments (0)