Routing Visitors by Geography with Netlify Edge Functions
Build a geolocation redirect with Netlify Edge Functions: Deno handler, netlify.toml routing, local CLI testing, production verification, and rollback options.
12 Sept 2026, 09:21 UTC

The problem: redirects that don't wait for the origin
You want visitors from Germany to land on /de/ and everyone else on the default site, without shipping client-side JavaScript that flashes the wrong page first, and without paying the latency cost of a round trip to a serverless function in a single region. Netlify Edge Functions run on Deno at the CDN's points of presence, so the redirect decision happens close to the visitor before the request ever reaches your origin.
This guide builds a working geolocation redirect, shows how to test it locally, and covers the failure modes that matter: redirect loops, missing context, and Deno's different module rules.
Prerequisites
- A site deployed on Netlify (any plan; Edge Functions are available broadly, but check current plan limits in the dashboard).
- Netlify CLI installed (
npm install -g netlify-cli) and authenticated withnetlify login. - Your project linked to the site (
netlify linkrun from the project root). - Basic familiarity with JavaScript. No Deno experience is required for this example, but note that Edge Functions do not run Node.js — there is no
package.jsonresolution and no built-in Node APIs likefs.
How the pieces fit
An Edge Function is a TypeScript or JavaScript file in netlify/edge-functions/ that exports a default handler. You declare which URL paths invoke it in netlify.toml. When a request matches, your code receives the Request plus a context object containing geolocation data derived from the visitor's IP, and it returns a Response — or calls context.next() to pass the request through unchanged.
Write the function
Create netlify/edge-functions/geo-redirect.ts:
import type { Context } from "@netlify/edge-functions";
const LOCALE_MAP: Record = {
DE: "/de/",
AT: "/de/",
FR: "/fr/",
};
export default async (request: Request, context: Context) => {
const url = new URL(request.url);
// Never redirect requests already on a localized path or for assets.
if (
url.pathname.startsWith("/de/") ||
url.pathname.startsWith("/fr/") ||
url.pathname.includes(".")
) {
return context.next();
}
const country = context.geo?.country?.code;
const target = country ? LOCALE_MAP[country] : undefined;
if (target) {
return Response.redirect(new URL(target, request.url), 302);
}
return context.next();
};Three details here prevent common production bugs:
- The localized-path guard stops redirect loops. Without it, a German visitor redirected to
/de/would be redirected again on every subsequent request if you later map paths differently. Guard clauses are cheap; loops are not. - The dot check skips static assets. Requests for
/styles.cssor/logo.svgshould never be redirected to a locale prefix. - A missing country falls through. Geolocation is best-effort.
context.geocan be incomplete (VPNs, privacy relays, unrecognized IPs), so always have a default path that works.
Declare the route
In netlify.toml at the project root:
[[edge_functions]]
path = "/*"
function = "geo-redirect"The path pattern controls which requests invoke the function. /* means everything; the in-function guards decide what actually gets redirected. If you only want the redirect on the homepage, use path = "/" instead — narrower patterns mean fewer invocations and lower cost.
Test locally with the CLI
Run netlify dev from your project root (no special permissions needed; it starts a local server, typically on port 8888). The CLI simulates the edge runtime, but note a key limitation: local geolocation data is stubbed or absent, so context.geo.country will usually be undefined and your function will fall through to context.next(). That's correct behavior — it verifies the fall-through path, not the redirect path.
To exercise the redirect locally, temporarily hardcode a country:
const country = "DE"; // TEMPORARY: local testing onlyThen request http://localhost:8888/ with curl and inspect the response:
curl -I http://localhost:8888/You should see HTTP/1.1 302 and a location: /de/ header. Remove the hardcoded line before committing — a pinned country in production redirects everyone.
Deploy and verify
Deploy with netlify deploy --prod (or push to your connected Git branch). Then verify from the outside:
- Open the site in a browser and check the response headers in DevTools. Netlify marks edge-invoked responses; look for evidence the function ran, such as your redirect taking effect or headers you explicitly set.
- Test the redirect path with a VPN exit node in one of your mapped countries, or ask a colleague in that region. There is no reliable way to spoof
context.geovia request headers — it comes from Netlify's IP lookup, which is exactly what makes it trustworthy. - Open Logs > Edge Functions in the Netlify dashboard to confirm invocations and check for uncaught exceptions. An edge function that throws returns an error to the visitor, so add
console.logof the country code during initial rollout and remove it later.
A practical smoke check: curl -sI https://your-site.netlify.app/ | grep -i location from a network in a mapped country should show the locale prefix; from an unmapped country it should show nothing and return 200.
Limits and trade-offs to plan around
- Execution time and memory are tightly constrained compared to standard Netlify Functions (which run on AWS Lambda). Edge Functions are for fast decisions — redirects, header rewrites, cookie-based A/B bucketing, auth token checks — not for rendering pages or calling slow APIs. If you need an upstream API call, budget its latency carefully; a slow fetch at the edge is still slow.
- Deno, not Node. npm packages that depend on Node built-ins will fail. Prefer web-standard APIs (
fetch,URL,crypto.subtle) and check that any import is Deno-compatible. - IP geolocation is approximate. Country-level accuracy is generally good; city-level is not. Never use it as a security boundary — a determined user with a VPN bypasses it trivially. For hard geographic restrictions (licensing, sanctions), treat this as a convenience layer, not enforcement.
- Every matched request is an invocation. Broad
pathpatterns on high-traffic sites add up against plan quotas. Check your current usage and limits in the dashboard.
Recovery options
Because this changes live routing behavior, know how to back out:
- Fastest: remove or comment out the
[[edge_functions]]block innetlify.tomland redeploy. Unmatched requests pass straight to your site. - Safer partial rollback: change the function to unconditionally
return context.next()and redeploy — this disables behavior while keeping the deployment pipeline intact for debugging. - Full rollback: use the deploy list in the Netlify dashboard to republish a previous deploy that predates the function.
If visitors report wrong-locale redirects after launch, check the function logs first: a missing guard clause or an over-broad LOCALE_MAP entry is almost always the cause, and both are visible in a single log line.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.