Edge functions, redirects and rewrites
Redirects and rewrites at the edge, the difference between rewriting and proxying, edge runtime limits, geo and header-based routing, and simple A/B routing.
Redirects and rewrites
| Directive | What the client sees | URL changes | Use it for |
|---|---|---|---|
| Redirect 301 | A redirect response | Yes | A permanent move |
| Redirect 302 or 307 | A redirect response | Yes | A temporary or context-dependent move |
| Rewrite | The final content directly | No | Friendly URLs, legacy paths, a new backend |
| Proxy | The final content directly | No | Serving another origin under your hostname |
| Custom response | Whatever you return | No | Maintenance pages, AB routing, auth gates |
# edge rules, expressed declaratively where the platform supports it
[[redirects]]
from = "/old-guide/*"
to = "/guides/:splat"
status = 301
force = true
[[rewrites]]
from = "/api/*"
to = "https://api.internal.example/:splat"
status = 200
[[headers]]
for = "/static/*"
[headers.values]
Cache-Control = "public, max-age=31536000, immutable"- Redirect chains waste a round trip each hop. Emit a single 301 from the edge to the final URL.
- Rewrites are invisible to the client, so absolute URLs generated by the application must still use the public hostname. Set the forwarded Host and X-Forwarded-Proto headers correctly.
- Never redirect to a URL that itself redirects. Audit the map after every change.
Edge functions and their limits
// an edge function: runs per request, close to the user
export default async function handler(request) {
const url = new URL(request.url);
// block a path before it reaches the origin
if (url.pathname.startsWith("/admin") && !request.headers.get("cookie")?.includes("session=")) {
return new Response("Not found", { status: 404 });
}
// route by country, with a sane fallback when the header is absent
const country = request.headers.get("cf-ipcountry") ?? "US";
if (url.pathname === "/" && ["DE", "FR", "NL"].includes(country)) {
return Response.redirect(new URL("/eu/", url), 302);
}
return fetch(request); // pass through to the origin
}| Limit | Typical value | Consequence |
|---|---|---|
| CPU time per request | 10-50 ms | No heavy parsing or hashing |
| Wall-clock timeout | Short at the edge, longer at a regional edge | No slow upstream calls |
| Memory | 128 MB | No large in-memory datasets |
| Bundle size | 1-5 MB | Few dependencies |
| Node APIs | Partial or absent | Not every library runs there |
| Subrequests | A small number per request | Chaining several APIs will fail |
| Caching | A KV store with eventual consistency | Not a database, not immediately consistent |
An edge function is not a server. It runs on the request path, it must finish quickly, and every millisecond it spends is added to every user's latency. Use it for routing, headers, redirects and small decisions - not for business logic that belongs behind a queue.
Routing and simple experiments
// a stable A/B assignment: hash the visitor id, not the request
async function assignVariant(request, env) {
const cookie = parseCookies(request.headers.get("cookie") ?? "");
let visitor = cookie.ab_id;
if (!visitor) {
visitor = crypto.randomUUID();
}
// hashing makes the assignment stable for the same visitor
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(visitor));
const bucket = new Uint8Array(digest)[0] % 100;
const variant = bucket < 10 ? "b" : "a"; // 10 percent to the variant
const response = await fetch(request);
const headers = new Headers(response.headers);
headers.append("Set-Cookie", "ab_id=" + visitor + "; Path=/; Max-Age=2592000; SameSite=Lax");
headers.set("x-ab-variant", variant);
return new Response(response.body, { status: response.status, headers });
}- Assign the variant from a stable identifier so a user does not flip between versions on every request.
- Keep the cache key independent of the variant, or you will cache the same URL twice and lose the traffic mixing.
- Do not split a cached response at the edge by mutating the body; that defeats the cache entirely. Split the HTML into a cached shell and a small variant fragment.
- Never experiment on a checkout or a payment path without a way to force a single variant for a support case.
- Record the variant on every event so the analysis is possible afterwards.
💡
Every edge rule is production code with the shortest feedback loop in your stack - it applies to every request immediately. Keep the rule set small, review changes like code, and always have a way to disable a rule in one action.
FAQ
Rewrite or proxy?
They are the same mechanism viewed from different sides: the client URL stays the same and the content comes from elsewhere. Choose based on the platform's naming and whether the origin needs the original host header.
Can I run a database query in an edge function?
Technically yes with an HTTP-based database API, but each query adds latency on the request path and connection pooling is difficult. Prefer a regional function or a cache.
Related
Edge architecture: personalisation and A/B testing at the edge How a CDN serves your content
Last refreshed 2026-09-18.