API Gateway at the Edge

Running an API gateway at the edge moves authentication, routing, rate limiting and payload transformation onto the CDN’s Points of Presence so requests are shaped and rejected before they ever reach your origin.

Deploying an API Gateway at the Edge shifts request handling, authentication and routing logic to compute that runs within a few milliseconds of the end user. Instead of backhauling every request to a single regional gateway sitting in front of your cluster, you execute the same logic at hundreds of locations. The result is lower tail latency, dramatically reduced origin load, and a smaller attack surface: malformed, unauthenticated and abusive traffic is dropped at the perimeter. This guide covers the DNS plumbing, provider-specific routing and validation code, the comparison matrix you need to pick a platform, and the operational procedures for deploying, debugging and rolling back an edge gateway in production.

Key implementation priorities:

  • Shift from a centralized regional gateway to distributed routing that runs at the PoP closest to each caller.
  • Validate JWTs and enforce rate limiting at the edge so unauthenticated and abusive traffic never reaches the origin.
  • Wire custom and wildcard domains to the gateway with apex-safe DNS and strict TLS termination.
  • Instrument request tracing, failover and rollback so a bad deploy is reverted in seconds, not minutes.
Request flow through an edge API gateway A client request enters the nearest PoP, passes through TLS termination, JWT validation, rate limiting and routing, then either proxies to a healthy origin or is rejected at the edge. Edge API gateway request path Client TLS request Nearest PoP — edge runtime 1. TLS terminate HSTS, SNI host 2. JWT validate WebCrypto verify 3. Rate limit KV sliding window 4. Route + rewrite match, transform Reject at edge 401 / 403 / 429 no origin hit Origin healthy pool

Core Architecture & DNS Configuration

Mapping a custom domain to an edge compute endpoint starts with DNS, and the apex is where most teams trip. A bare root domain (example.com) cannot use a standard CNAME because RFC 1034 forbids a CNAME coexisting with the SOA and NS records that must live at the zone apex. The fix is provider-side synthesis: CNAME flattening or ALIAS/ANAME records resolve the target at query time and return A/AAAA answers, so the apex points at the edge network without violating the spec or adding a client-visible round trip.

Subdomains are simpler. Point api.example.com at the provider’s edge hostname with a normal proxied CNAME. For multi-tenant SaaS, map a wildcard (*.api.example.com) to a single gateway route and parse the Host header inside the worker to isolate each tenant; this avoids one route per customer and keeps your routing table flat. Whatever shape you choose, terminate TLS at the PoP and enforce Strict-Transport-Security so a downgrade attack can never strip encryption between the client and the edge.

Name shape, edge mechanism and the answer resolvers cache Three rows compare an apex name, a delegated subdomain and a wildcard label, showing which edge mechanism resolves each one and what a resolver finally stores in cache. Name shape, mechanism, cached answer Name in the zone Edge mechanism Answer on the wire example.com apex: SOA + NS present CNAME flattening resolved at query time A / AAAA answer no extra client round trip api.example.com delegated subdomain proxied CNAME one route, one worker edge anycast address TLS terminated at the PoP *.api.example.com wildcard label one gateway route Host header picks tenant same address for all isolation lives in code A flattened apex inherits the target's TTL, so the apex answer expires on the provider's schedule.

One consequence of flattening catches teams by surprise: the apex no longer honors the TTL you typed into the record. The provider answers with the TTL of the record it resolved, which for most edge networks is a short value chosen by the platform. That is usually what you want during a migration, but it means you cannot lengthen apex caching to reduce query volume the way you can on a normal A record, and any capacity math that assumed a 3600-second apex TTL is wrong.

The Host header deserves the same care as the DNS record. Wildcard gateways route on it, tenant isolation depends on it, and a request that arrives with a Host your worker does not recognize should get a 404 rather than falling through to a default backend. Normalize case, strip any port suffix, and reject values containing anything outside the hostname character set before using them as a lookup key — a Host value is attacker-controlled input, not trusted metadata.

TTL strategy matters during cutover. Before migrating an existing gateway, lower the record TTL well ahead of time so resolvers stop caching the old answer; review Mastering TTL Strategies for the rollback-friendly values to use during a migration window.

Verification commands

# Verify apex/subdomain resolution and inspect edge response headers
dig +short api.example.com CNAME
dig +short example.com A          # flattened apex returns A records, not a CNAME
curl -sI https://api.example.com/health | grep -E 'HTTP|server|cf-ray|strict-transport'

Expected output:

edge-gateway.provider.net.
104.18.12.34
HTTP/2 200
server: cloudflare
cf-ray: 8a1b2c3d4e5f6a7b-IAD
strict-transport-security: max-age=63072000; includeSubDomains; preload

Infrastructure-as-Code (Terraform)

resource "cloudflare_record" "edge_api" {
  zone_id = var.zone_id
  name    = "api"
  type    = "CNAME"
  content = "edge-gateway.provider.net"
  proxied = true   # orange-cloud: routes through the edge runtime + TLS
  ttl     = 1      # 1 = "automatic" when proxied
}

resource "cloudflare_record" "apex_alias" {
  zone_id = var.zone_id
  name    = "@"
  type    = "CNAME"             # Cloudflare flattens this at the apex
  content = "edge-gateway.provider.net"
  proxied = true
}

Implementing Edge Routing Rules

An edge routing engine evaluates each request against a precedence chain. The canonical order is exact match, then prefix match, then regex match, then a fallback origin. Get the order wrong and you create cache collisions or silently route /v2/users into the /v1 pool. Define rules declaratively in version control so the precedence is auditable, not buried in a dashboard.

Header-based routing unlocks safe deployment patterns: inject X-Canary-Release: true or a tenant identifier and steer that slice to an isolated backend pool while everyone else hits the stable origin. Pair every route with a health-checked fallback so a 5xx from the primary fails over automatically rather than surfacing to the client. For the full matcher syntax and how route patterns interact with zones, see Cloudflare Workers Routing.

Route configuration via wrangler.toml

name = "edge-api-gateway"
main = "src/index.js"
compatibility_date = "2024-09-23"

[[routes]]
pattern   = "api.example.com/v2/*"
zone_name = "example.com"

[[kv_namespaces]]
binding = "RATE_LIMITS"
id      = "f3a9c2e1b7d4488e9a01c5d6e7f80912"

Deploy with:

npx wrangler deploy --env production

Routes are declared in wrangler.toml or the dashboard — wrangler routes add is not a valid command. A deploy propagates to every PoP within seconds, which is exactly why rollback also needs to be fast and deliberate.

Security & Request Transformation

Running auth at the edge removes the per-request authentication cost from your origin entirely. Validate JWT signatures synchronously with the runtime’s native WebCrypto so an expired or forged token is rejected with 401/403 before any proxy call. Never forward an unverified payload upstream. For the full signing-algorithm matrix, kid rotation and JWKS caching, follow the deep dive on JWT Validation at the Edge with Cloudflare Workers.

Layer abuse protection on top of auth. A sliding-window counter in an edge KV or Durable Object stops burst abuse per API key or IP; the implementation patterns and trade-offs live in Rate Limiting API Requests at the Edge. For signature-based attacks — SQLi, path traversal, known-bad bots — front the gateway with managed rules as described in WAF & Rate Limiting at the Edge, so the worker only ever sees traffic that already passed the firewall.

On the transformation side, strip internal headers like X-Internal-Debug before proxying, inject a unique X-Request-ID for tracing, and rewrite upstream paths so clients never learn your internal routing.

Secret management

# Inject signing keys into the edge runtime; never commit them to wrangler.toml
npx wrangler secret put JWT_SECRET_KEY --env production

Expected output:

✔ Secret 'JWT_SECRET_KEY' uploaded successfully to environment 'production'

Platform Implementation

Cloudflare Workers

Workers run a V8 isolate at every PoP, so JWT verification and the origin proxy happen in the same hot path with no cold-start penalty. This snippet validates a bearer token, enforces a coarse per-key limit, then proxies to a private origin with tracing headers attached.

export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);

    const authHeader = request.headers.get('Authorization');
    if (!authHeader?.startsWith('Bearer ')) {
      return new Response('Unauthorized', { status: 401 });
    }
    const token = authHeader.slice(7);
    const isValid = await verifyJWT(token, env.JWT_SECRET);
    if (!isValid) return new Response('Invalid Token', { status: 403 });

    // Sliding-window rate limit keyed on the token subject
    const key = `rl:${await sha256(token)}`;
    const count = parseInt((await env.RATE_LIMITS.get(key)) ?? '0', 10);
    if (count >= 100) return new Response('Too Many Requests', { status: 429 });
    ctx.waitUntil(env.RATE_LIMITS.put(key, String(count + 1), { expirationTtl: 60 }));

    const originReq = new Request(`https://api-origin.internal${url.pathname}${url.search}`, request);
    originReq.headers.set('X-Forwarded-By', 'edge-gateway');
    originReq.headers.set('X-Request-ID', crypto.randomUUID());
    originReq.headers.delete('X-Internal-Debug');

    return fetch(originReq);
  }
};

AWS Lambda@Edge / CloudFront Functions

On AWS the gateway splits across two runtimes. CloudFront Functions (lightweight, sub-millisecond, JS) handle header rewrites and cheap auth at viewer-request; Lambda@Edge (full Node/Python, higher latency, cold starts) handles anything needing network calls or larger compute. A viewer-request function is the right place for a fast JWT gate.

// CloudFront Function (viewer-request) — header normalization + token presence
function handler(event) {
  var req = event.request;
  var auth = req.headers.authorization;
  if (!auth || auth.value.indexOf('Bearer ') !== 0) {
    return { statusCode: 401, statusDescription: 'Unauthorized' };
  }
  req.headers['x-request-id'] = { value: event.context.requestId };
  return req; // forward to the cache / Lambda@Edge origin-request handler
}

Azure Front Door

Azure Front Door keeps routing and WAF policy declarative and hands anything compute-heavy to a co-located Azure Function. Routes bind a domain and a path pattern to an origin group, and a rule set attached to the route performs the header work. The important operational detail is that origin groups carry their own health probe and load-balancing settings, so failover is a property of the group rather than something you code.

# Bind /v2/* on the custom domain to a health-probed origin group
az afd route create \
  --resource-group edge-rg \
  --profile-name edge-fd \
  --endpoint-name api-endpoint \
  --route-name v2-route \
  --origin-group api-origin-group \
  --patterns-to-match "/v2/*" \
  --forwarding-protocol HttpsOnly \
  --https-redirect Enabled \
  --rule-sets gateway-headers

GCP Cloud CDN with a global load balancer

On Google Cloud the gateway is the global external Application Load Balancer: a URL map holds the path matchers, backend services hold the health checks, and Cloud CDN caches in front of them. There is no per-PoP compute in this path, so authentication either runs in the backend service or in a Cloud Run service placed behind a dedicated path rule.

# Route /v2/* to a versioned backend service, everything else to the default
gcloud compute url-maps add-path-matcher api-url-map \
  --path-matcher-name=v2-matcher \
  --default-service=api-default-backend \
  --backend-service-path-rules="/v2/*=api-v2-backend" \
  --new-hosts=api.example.com

Fastly Compute@Edge and VCL

Fastly gives you two data planes. Compute@Edge runs WebAssembly with a higher memory ceiling than a V8 isolate, which makes it the strongest option for body-heavy work. Classic VCL remains the most direct way to express precedence, synthetic responses and backend selection, and its state machine makes the evaluation order explicit rather than implied.

sub vcl_recv {
  # Reject unauthenticated API calls before they reach the backend
  if (req.url ~ "^/v2/" && req.http.Authorization !~ "^Bearer ") {
    error 401 "Unauthorized";
  }
  # Prefix route: v2 traffic to the versioned backend
  if (req.url ~ "^/v2/") {
    set req.backend = F_v2_origin;
  }
}

sub vcl_error {
  # Serve the 401 as a JSON body instead of Fastly's default HTML page
  if (obj.status == 401) {
    set obj.http.Content-Type = "application/json";
    synthetic {"{"error":"unauthorized"}"};
    return(deliver);
  }
}

A backend .probe block keeps the failover decision at the edge rather than in DNS, which is the difference between a ten-second recovery and a TTL-bound one:

backend F_v2_origin {
  .host = "v2-origin.internal.example.com";
  .port = "443";
  .ssl = true;
  .first_byte_timeout = 15s;
  .probe = {
    .request = "HEAD /health HTTP/1.1" "Host: v2-origin.internal.example.com" "Connection: close";
    .expected_response = 200;
    .interval = 5s;
    .timeout = 2s;
    .window = 5;
    .threshold = 3;
  }
}

Platform Comparison

Provider Mechanism Wire behavior Failover / Notes
Cloudflare Workers V8 isolate per PoP, KV/Durable Objects Validates + proxies in one hot path; no cold start Health-checked origin failover; Load Balancing add-on for pools
AWS Lambda@Edge Node/Python at regional edge caches Cold starts 100ms+; runs at CloudFront events CloudFront origin groups give primary/secondary failover
AWS CloudFront Functions Lightweight JS at viewer events Sub-ms, no network I/O; header + auth only Pair with Lambda@Edge for heavier logic
Azure Front Door Declarative routing + WAF + Functions Rules at the edge, compute backhauled to Functions Built-in priority/weighted backend failover
GCP Cloud CDN + GLB Global LB path matchers Routing at LB, compute at backend services LB health checks drive automatic backend draining
Fastly Compute@Edge / VCL Wasm or VCL data plane VCL gives explicit precedence + synthetic responses Backend .probe health checks with auto fallback

Deployment & Operational Procedure

  1. Lower DNS TTL. Drop the record TTL to 60s at least 48 hours before cutover so resolvers release the old answer quickly. See Mastering TTL Strategies.
  2. Stage on a canary host. Deploy the gateway to api-canary.example.com and route only X-Canary-Release: true traffic to it.
  3. Load and validate. Replay production traffic; confirm 401/403/429 rates and p99 latency match expectations in wrangler tail.
  4. Promote. Update the production [[routes]] block and run npx wrangler deploy --env production.
  5. Watch. Tail logs and dashboards for the first 15 minutes; compare cf-ray/x-vercel-id traces against origin logs.
  6. Record. Commit the routing manifest so the change is auditable and revertible.

The order is not arbitrary. DNS is the slowest lever you own, so it moves first and moves early; code deploys are the fastest, so they move last when the blast radius is smallest. Between those two sits the canary, which exists to answer one question — does the new gateway produce the same status-code distribution as the old one under real traffic? Until you can answer that from logs rather than from a synthetic check, promotion is a guess.

Cutover timeline for an edge API gateway Four milestones plotted on a time axis: lowering the record TTL two days ahead, running a header-gated canary, promoting the production route, and holding a verification and rollback window afterwards. Cutover milestones on the clock Lower the TTL 60 s, 48 h ahead resolvers let go Canary route header-gated slice replay traffic Promote wrangler deploy global in seconds Hold + verify tail logs 15 min revert stays armed T-48 h T-30 min T-0 T+15 min Drift in 5xx rate or p99 at any tick reverts the manifest — no DNS change required. Close the window only when edge traces and origin logs agree on the same request IDs.

Rollback & failover protocol

  • Keep a versioned routing manifest in Git; a rollback is git revert plus a redeploy, or toggling the route off in the dashboard — propagation is under 30 seconds.
  • Configure a circuit breaker that drops a degraded origin after 3 consecutive 504 timeouts and serves the secondary pool.
  • For multi-origin pools, drive failover from edge health checks rather than DNS so recovery is measured in seconds.

Debugging & Observability

Distributed gateways scatter logs across PoPs, so correlation IDs are non-negotiable. Trace a request by inspecting cf-ray, x-vercel-id or x-edge-location, then join those identifiers against the X-Request-ID you injected upstream. Emit structured JSON at the edge and ship it to your aggregator so a single request can be reconstructed end to end.

Stream live edge logs while validating a deploy:

npx wrangler tail --format pretty --env production

Expected output:

[2024-01-15 10:30:00] GET  /v2/users  -> 200 OK (12ms) ray=8a1b2c3d
[2024-01-15 10:30:01] POST /v2/auth   -> 401 Unauthorized (4ms) [JWT_EXPIRED]
[2024-01-15 10:30:02] GET  /v2/report -> 429 Too Many Requests (2ms) [RATE_LIMIT]

Use the platform emulator (wrangler dev, vercel dev) to simulate origin timeouts, network partitions and cache-bypass scenarios before promoting. For framework-aware routing contexts and matcher semantics on Next.js, review Vercel Edge Middleware.

Troubleshooting Scenarios

A 5xx storm minutes after promotion

The symptom is a step change in 502/504 at the edge while origin dashboards look calm. That combination almost always means the gateway is failing before it reaches the origin: a binding that exists in the canary environment but not in production, an origin hostname that resolves internally but not from the PoP, or a fetch to a private address the edge network cannot route. Start by separating edge-generated errors from origin-generated ones — an origin 500 carries your application’s headers, an edge 502 does not.

# Compare the two populations over the last minute of live traffic
npx wrangler tail --env production --format json \
  | jq -r 'select(.outcome != "ok" or (.response.status // 0) >= 500)
           | [.event.request.url, .response.status, (.logs[0].message[0] // "")] | @tsv'

If the failing requests share one route pattern, roll that route back rather than the whole worker. If they share one PoP, the problem is a regional origin path and the fix is a health-check-driven failover to the secondary pool, not a code change.

Route precedence collision after adding a version

Adding api.example.com/v2/* alongside an existing api.example.com/* is the classic collision. Cloudflare picks the most specific matching route, but a trailing-slash difference or a missing * changes what counts as specific, and the result is a worker that serves /v2 traffic with /v1 logic. Enumerate what is actually deployed instead of trusting the file you last edited:

curl -s -H "Authorization: Bearer $CF_API_TOKEN" \
  "https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/workers/routes" \
  | jq -r '.result[] | [.pattern, .script] | @tsv'

Fix by deleting the overlapping pattern rather than adding a more specific one on top; each additional overlapping route makes the next incident harder to reason about. Keep the routing manifest ordered from most specific to least specific in version control even though the platform sorts it itself — the file is what your team reads during an incident.

Secret drift between environments

A gateway that returns 401 for every token in production while staging is healthy is usually reading a secret that was never uploaded to the production environment, or that was rotated on the identity provider side without being rotated at the edge. Secrets are per-environment, and a wrangler secret put without --env production writes to the wrong place silently.

npx wrangler secret list --env production
npx wrangler secret list --env staging

The remedy is process, not code: rotate through a script that writes both environments, and add a startup assertion that fails loudly when an expected secret is absent instead of falling through to an empty string. An empty signing key does not throw — it just makes every signature invalid.

Edge Cases & Production Warnings

Scenario Impact Mitigation
Cold starts on heavyweight runtimes (Lambda@Edge) during spikes Elevated p99 latency; timeouts for strict-SLA endpoints Keep auth on isolate runtimes (Workers, CloudFront Functions); pre-warm with synthetic pings; use always-on tiers for critical paths
DNS propagation lag during gateway migration Split-brain: some users hit the legacy gateway, others the new edge Lower TTL 48h ahead; dual-write during the window; steer with health-checked DNS
CORS preflight cached at PoPs Stale OPTIONS policy blocks legitimate cross-origin calls after an update Send Cache-Control: no-cache on OPTIONS; version CORS via KV; never aggressively cache preflights
CPU time limits exceeded Request killed mid-execution (typically 10–50ms free, up to 30s enterprise) Offload heavy work to a queue or origin; keep the edge path to validate-route-proxy
Secrets committed to wrangler.toml Key leakage in Git history Always use wrangler secret put; rotate JWT_SECRET_KEY on a schedule

Beyond the table, a handful of failure modes only surface once real traffic is on the gateway:

  • Request body consumed twice. Reading await request.json() for a validation check and then passing the same request object to fetch sends an empty body upstream. Clone first, or read once and rebuild the request with the bytes you already have.
  • waitUntil work that silently fails. Counters and log shipping scheduled with ctx.waitUntil run after the response is sent, so their errors never reach the client and never show up as a 5xx. Log inside the promise or you will not know it stopped working.
  • Compression applied twice. If the origin already returns content-encoding: br and the gateway re-encodes, clients get garbage. Decide at one layer only and strip the header you are not honoring.
  • Host header rewritten unintentionally. Building new Request(newUrl, request) inherits the original headers but the new URL sets the SNI. An origin that virtual-hosts on Host will serve the wrong site, and the error looks like a routing bug rather than a header bug.
  • Wildcard routes swallowing the health check. A *.api.example.com route matches your own monitoring hostname too, which means a gateway outage takes the health endpoint with it. Keep at least one path that bypasses the worker entirely.
  • Clock-sensitive logic across PoPs. Token expiry, counter windows and cache TTLs all read a local clock. Never compare timestamps produced at two different PoPs without a tolerance; the drift is small but not zero.

Frequently Asked Questions

How does an edge API gateway differ from a traditional centralized gateway? An edge gateway executes routing, authentication and transformation at distributed PoPs within milliseconds of the user, so unauthenticated and abusive traffic is rejected at the perimeter. A centralized gateway backhauls every request to one region first, adding latency and concentrating load on a single failure domain.

Can I use an edge API gateway for WebSocket or gRPC traffic? Yes, with caveats. Cloudflare supports WebSockets natively at the edge, so you can authenticate the upgrade request and proxy the socket. gRPC needs HTTP/2 passthrough or translation to REST/JSON at the edge because its binary framing is not something most edge runtimes parse directly.

What happens if the edge compute environment times out? Edge platforms enforce strict CPU limits — roughly 10–50ms on free tiers, up to 30s on enterprise. If you exceed it the request is terminated. Keep the hot path to validate, rate limit, route and proxy, and push any heavy computation to a queue or the origin.

How do I keep a private origin from being reached directly? Put the origin behind an authenticated tunnel or a mutual-TLS listener and refuse plain requests, then have the gateway present the client certificate or tunnel credential. An IP allowlist of edge ranges is weaker but better than nothing; on its own it fails the moment someone finds the origin hostname in a certificate transparency log or an old DNS record.

Where should I enforce rate limiting versus WAF rules? Run WAF signature rules first so known-bad and malformed requests are dropped before your worker executes, then apply per-key or per-IP rate limiting inside the gateway for application-level abuse. See WAF & Rate Limiting at the Edge and Rate Limiting API Requests at the Edge.

Back to Edge Routing & Serverless Function Architecture