Implementing Geo-Routing with Edge Functions for Latency Reduction

This guide details the exact implementation of geographic traffic routing at the CDN edge to minimize Time-To-First-Byte (TTFB) and regional latency. By intercepting requests in an edge function before they reach the origin, you read a provider-injected country code, map it to the nearest regional backend, and rewrite the request — all without a client-visible redirect. Properly configured Geo-Targeted Traffic Routing ensures users hit the closest compute node instead of a distant default origin. Integrating these patterns into your broader Edge Routing & Serverless Function Architecture maintains low-latency API delivery and fault tolerance, and pairs naturally with weighted load balancing across multi-region origins when a region has more than one healthy backend.

Key implementation objectives:

  • Leverage provider-injected geolocation signals (request.cf.country, x-vercel-ip-country) instead of slow IP-to-geo lookups.
  • Map country codes to optimized regional origins using internal edge fetch rewrites, not 3xx redirects.
  • Implement strict fallback logic for missing headers, unknown regions, and origin failures.
  • Validate routing decisions and TTFB improvements from multiple Points of Presence (PoPs) with curl and edge logs.
Edge geo-routing request flow A client request reaches the nearest edge PoP, which reads the country code, looks up a region, and rewrites the fetch to a regional origin, falling back to the global origin on failure. Client country=DE Edge function read cf.country lookup region rewrite fetch EU origin nearest, low TTFB AP origin other regions Global origin fallback / default match EU 5xx / no header

Prerequisites & Edge Provider Configuration

Establish DNS routing and enable geolocation signal injection at the CDN level before deploying functions. The country code is derived from the client IP by the edge network, so it is only available once the platform feature is active.

Configure CNAME flattening or ALIAS records for root domains to avoid apex resolution failures, and set DNS TTL to 300s on the routed hostnames so failover and origin changes take effect quickly — see Mastering TTL Strategies for the trade-offs. Verify propagation and record readiness using standard DNS utilities.

dig +short CNAME your-domain.com
dig +short TXT _geo-config.your-domain.com

Expected output: the CNAME line resolves to your CDN hostname (for example your-domain.com.cdn.cloudflare.net.), and the TXT line returns your routing config marker. An empty CNAME result on an apex record means you need a flattened/ALIAS record rather than a raw CNAME.

Platform constraints:

Provider Geo signal How to enable CPU budget
Cloudflare Workers request.cf.country Available by default; add an IP Geolocation header in Network for non-Worker reads 10ms (free), 50ms+ (paid)
Vercel Edge x-vercel-ip-country Injected automatically on Pro+ ~100ms wall, strict
AWS CloudFront / Lambda@Edge CloudFront-Viewer-Country Add the header to the cache behavior’s origin request policy 5s viewer-request

Core Routing Logic Implementation

Write the edge function to parse the country code, map it to a region, and rewrite the request via an internal fetch. This approach prioritizes a server-side rewrite over an external 3xx redirect, preserving the session, query string, HTTP method, and body while eliminating an extra client round trip.

Step 1 — Read the country code and resolve a region

Extract the ISO 3166-1 alpha-2 country code from the edge-provided field, then resolve it to a region key via a Map. Keep the lookup table flat and in module scope so it is built once per isolate, not per request, staying well inside the CPU budget.

const REGION_BY_COUNTRY = new Map(
  Object.entries({
    DE: 'eu', FR: 'eu', IT: 'eu', ES: 'eu', NL: 'eu', PL: 'eu',
    SE: 'eu', BE: 'eu', AT: 'eu', FI: 'eu', GB: 'eu',
    JP: 'ap', KR: 'ap', SG: 'ap', AU: 'ap', IN: 'ap',
    CN: 'ap', TH: 'ap', VN: 'ap',
  })
);

const ORIGIN_BY_REGION = {
  eu: 'https://eu-origin.example.com',
  ap: 'https://ap-origin.example.com',
  global: 'https://global-origin.example.com',
};

function resolveOrigin(country) {
  const region = REGION_BY_COUNTRY.get(country) || 'global';
  return ORIGIN_BY_REGION[region];
}

Note: request.cf.country returns country codes like DE or JP — never region codes like EU. Unknown or missing values fall through to global, which is the safe default.

Step 2 — Rewrite the request on Cloudflare Workers

Swap only the hostname so the path, query, headers, and method are carried over unchanged. This is a transparent rewrite — the browser URL never changes.

export default {
  async fetch(request, env, ctx) {
    if (env.ENABLE_GEO_ROUTING === 'false') {
      return fetch(request);
    }
    const country = request.cf?.country || 'US';
    const targetOrigin = resolveOrigin(country);

    const url = new URL(request.url);
    url.hostname = new URL(targetOrigin).hostname;

    const routed = new Request(url, request);
    routed.headers.set('x-geo-country', country);
    return fetch(routed);
  },
};

Expected behavior: a request from Germany to https://your-domain.com/api/v1/health is fetched from eu-origin.example.com while the client still sees your-domain.com. The x-geo-country header gives you a routing breadcrumb for logs and verification.

Step 3 — Rewrite the request on Vercel Edge Middleware

On Vercel the equivalent uses NextResponse.rewrite so the response is served as if it came from the matched path. Scope the matcher tightly to avoid running middleware on static assets.

import { NextRequest, NextResponse } from 'next/server';

export const config = { matcher: '/api/:path*' };

export function middleware(req: NextRequest) {
  if (process.env.ENABLE_GEO_ROUTING === 'false') return NextResponse.next();

  const country = req.headers.get('x-vercel-ip-country') || 'US';
  const origin =
    country === 'DE' || country === 'FR'
      ? 'https://eu-api.your-domain.com'
      : null;

  if (origin) {
    const url = new URL(req.nextUrl.pathname + req.nextUrl.search, origin);
    return NextResponse.rewrite(url);
  }
  return NextResponse.next();
}

Expected output: requests from DE/FR resolve against the EU API host; all others continue to the default origin via NextResponse.next().

Rollback procedure: Keep an ENABLE_GEO_ROUTING flag in edge KV or environment variables. Setting it to false reverts every request to the global origin instantly, with no redeploy. This same kill switch is the first lever to pull during any geo-routing incident.

Verification — Latency Validation & Diagnostic Commands

Verify routing decisions and measure TTFB improvements across global PoPs. Use a verbose curl to capture the timing breakdown and confirm which origin served the request.

curl -s -o /dev/null \
  -w 'HTTP_CODE: %{http_code}\nTTFB: %{time_starttransfer}s\nDNS: %{time_namelookup}s\nCONNECT: %{time_connect}s\n' \
  -H 'Accept: application/json' \
  https://your-domain.com/api/v1/health

Expected output for a correctly routed nearby origin:

HTTP_CODE: 200
TTFB: 0.071s
DNS: 0.012s
CONNECT: 0.025s

To confirm the routing branch, inspect headers and simulate a regional client IP. On Cloudflare you can spoof the source country for testing, and on Vercel you can use the local dev proxy:

curl -sI https://your-domain.com/api/v1/health \
  -H 'CF-Connecting-IP: 203.0.113.10' | grep -i -E 'x-geo-country|cf-ray|x-vercel-id'

Expected output shows the x-geo-country header you set in Step 2 plus a provider trace ID:

x-geo-country: DE
cf-ray: 8a1f2c3d4e5f6789-FRA

Watch live execution while you test with wrangler tail, which streams each routing decision so you can confirm the region resolved before the fetch fired:

npx wrangler tail --format pretty

Troubleshooting

Scenario 1 — Missing geolocation headers. Symptom: requests default to the global origin, raising latency and risking data-residency rules. Diagnosis: request.cf.country is undefined or x-vercel-ip-country is absent, typically because of privacy proxies, corporate VPNs, or Tor exit nodes that strip or mask the source IP. Fix: default to global (already handled by resolveOrigin), log the absence with a correlation ID, and optionally back-fill with a cached IP-to-geo lookup in edge KV for repeat clients.

Scenario 2 — Cold-start TTFB spikes during traffic surges. Symptom: the first request to a fresh isolate spikes 200–500ms, breaching SLOs. Diagnosis: concurrency exceeded warm capacity and the runtime initialized a new environment. Fix: pre-warm hot routes with synthetic cron traffic, cache the resolved origin per country in edge KV for 5–10 minutes so the decision skips recomputation, and keep module-scope tables small.

Scenario 3 — DNS caching delays after a regional origin IP rotation. Symptom: the edge routes to a decommissioned endpoint, returning connection timeouts or 502s. Diagnosis: the runtime DNS resolver cached A/AAAA records longer than your TTL during rapid infra changes. Fix: lower the origin hostname TTL to 60s, use CNAME flattening to bypass intermediate resolver caching, and update an edge KV routing table on health-check failure rather than waiting for DNS to converge.

Scenario 4 — Origin 5xx cascading into client-visible errors. Symptom: a regional origin outage returns 502/503 to users. Diagnosis: an unhandled fetch exception or 5xx is passed straight through. Fix: wrap the regional fetch in a timeout and fail over to the global origin on error or 5xx.

async function routedFetch(routed, fallbackOrigin) {
  try {
    const res = await fetch(routed, { signal: AbortSignal.timeout(3000) });
    if (res.status >= 500) throw new Error(`origin ${res.status}`);
    return res;
  } catch (error) {
    console.error('regional origin failed, failing over:', error);
    const url = new URL(routed.url);
    url.hostname = new URL(fallbackOrigin).hostname;
    return fetch(new Request(url, routed));
  }
}

Scenario 5 — Routing fights with country blocking. Symptom: users in a restricted country still reach a regional origin. Diagnosis: the geo-route runs before any access policy. Fix: evaluate blocks first — see blocking or redirecting traffic by country at the edge — and only resolve an origin for allowed countries.

Back to Geo-Targeted Traffic Routing

Frequently Asked Questions

Does edge geo-routing replace DNS-based GeoDNS? No. Edge functions operate after DNS resolution, so GeoDNS routes at the DNS level while edge functions route at the HTTP request level. The edge approach offers finer-grained control, header inspection, and dynamic fallbacks without waiting on DNS TTL changes.

How do I handle GDPR/CCPA compliance when routing by IP? Edge providers derive a country code without exposing the raw IP, so use only that code for routing and cache decisions and avoid logging raw IPs in your function. Disclose regional routing in your privacy policy and keep any IP-to-geo cache short-lived.

What is the maximum acceptable TTFB for edge-routed requests? Target under 150ms TTFB for edge-routed API calls. If TTFB exceeds 300ms, verify regional origin health, check for cold starts, and make sure no extra middleware is chaining before the fetch.

Can I use edge functions to route WebSocket connections by region? Yes, with limits. The function can inspect the initial HTTP upgrade request and route it to the nearest WebSocket gateway, but the persistent connection bypasses the function after the upgrade, so the initial routing decision must be correct.

How is this different from weighted load balancing? Geo-routing picks a region from the client’s country; weighted load balancing then distributes within a region across multiple origins by capacity. Combine them by resolving a region here and delegating the per-origin choice to weighted load balancing across multi-region origins.