Deploying Cloudflare Workers for Dynamic Request Routing
This guide provides production-grade instructions for deploying Cloudflare Workers to intercept, evaluate, and route HTTP traffic at the network edge. By leveraging V8 isolates and the Fetch API, you can implement conditional routing, path rewriting, and multi-origin failover entirely in the request path — before a packet ever reaches an origin. This architecture sits at the core of modern Edge Routing & Serverless Function Architecture, enabling real-time traffic steering based on headers, cookies, and geolocation data. The objective here is concrete: stand up a Worker that maps inbound requests to one of several origins, deploys cleanly to a staging environment, and is fully observable through wrangler tail and curl -I.
Key implementation objectives:
- Understand the module-Worker
fetchlifecycle and where request interception happens in Cloudflare’s edge - Configure
wrangler.tomlfor environment-specific route bindings and variables - Implement conditional routing using prefix matching, request headers, and
request.cf.country - Validate routing behavior and origin selection with
curl -I,dig, and livewrangler taillogs
Prerequisites & CLI Environment Setup
Establish the local development environment, authenticate with Cloudflare, and scaffold the Worker project. Crucially, ensure the DNS records that front your application are proxied (orange cloud enabled). Edge interception fails silently if proxying is disabled — the request bypasses Cloudflare entirely and your Worker never runs.
- Install Wrangler v3+ via npm (Node 18+) or Homebrew
- Authenticate using
wrangler loginand confirm your Account ID - Initialize the project with a JavaScript or TypeScript template
- Confirm DNS proxy status in the Cloudflare dashboard before proceeding
npm install -g wrangler
wrangler --version # expect 3.x or newer
wrangler login # opens a browser OAuth flow
wrangler whoami # prints account email + Account ID
wrangler init my-edge-router
Expected output: wrangler whoami returns a table with your authenticated email and a 32-character Account ID. If it reports “Not logged in”, re-run wrangler login and confirm the browser grant. Before writing any routing logic, decide whether you want Workers bound to specific route patterns or deployed under a *.workers.dev subdomain. For production traffic steering you almost always want named routes on your own zone, which we configure below. If you are still weighing platforms, the Cloudflare Workers vs AWS Lambda@Edge for Request Routing comparison covers cold-start, pricing, and cf object differences that influence this decision.
Step 1 — Core Routing Logic & Request Interception
Write the Worker script to parse incoming requests, apply conditional routing rules, and forward traffic to designated origins. This step directly implements the Cloudflare Workers Routing patterns required for production traffic steering. Use the modern module syntax (export default { async fetch() }) — the legacy addEventListener('fetch') style is deprecated and lacks clean access to env bindings.
- Match paths using
startsWith()orURLPatternfor predictable, RFC-compliant routing - Preserve original method, body, and headers by passing
requestintonew Request() - Always include fallback logic so unmatched routes never produce a
5xx
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
const routeMap = {
'/api/v1': 'https://legacy-api.example.com',
'/api/v2': 'https://modern-api.example.com',
'/static': 'https://cdn-assets.example.com',
};
for (const [prefix, origin] of Object.entries(routeMap)) {
if (url.pathname.startsWith(prefix)) {
const newUrl = origin + url.pathname.slice(prefix.length) + url.search;
return fetch(new Request(newUrl, request));
}
}
// Default: pass through unchanged to the zone's configured origin.
return fetch(request);
},
};
Expected behavior: a request to /api/v2/users is rewritten to https://modern-api.example.com/users while preserving the method, headers, and body. The new Request inherits everything from the original because request is passed as the second argument. Anything that does not match a prefix falls through to the default fetch(request), which preserves availability.
To layer in geographic steering, read request.cf.country (a two-letter ISO code populated by Cloudflare at the edge) and wrap the origin fetch in a try/catch for automatic failover:
export default {
async fetch(request, env, ctx) {
const url = new URL(request.url);
const country = request.cf?.country ?? 'US';
let origin = 'https://us-primary.example.com';
if (country === 'DE' || country === 'FR') origin = 'https://eu-primary.example.com';
if (country === 'JP' || country === 'KR') origin = 'https://apac-primary.example.com';
const target = origin + url.pathname + url.search;
try {
return await fetch(new Request(target, request));
} catch (err) {
ctx.waitUntil(logFailure(err, country)); // non-blocking
return fetch('https://global-fallback.example.com' + url.pathname, request);
}
},
};
function logFailure(err, country) {
return fetch('https://logs.example.com/ingest', {
method: 'POST',
body: JSON.stringify({ err: String(err), country, ts: Date.now() }),
});
}
Expected behavior: visitors resolved to Germany or France hit eu-primary, APAC traffic hits apac-primary, and everything else hits us-primary. If the selected origin throws a network error, the request silently re-fetches from the global fallback and the failure is logged out-of-band via ctx.waitUntil(), which keeps logging off the critical response path. This same cf-object pattern underpins Cloudflare Workers vs AWS Lambda@Edge for Request Routing, where the richness of request.cf is a frequent deciding factor. If your routing decisions depend on authenticated identity rather than geography, pair this with JWT Validation at the Edge with Cloudflare Workers to verify tokens before selecting an origin.
Step 2 — Configure wrangler.toml & Route Bindings
Define how the Worker maps to your zone. The routes array binds the Worker to specific URL patterns, and the [env.production] table isolates production configuration from staging so you can deploy independently without configuration drift.
name = "my-edge-router"
main = "src/index.js"
compatibility_date = "2024-09-23"
# Dynamic routing table stored in KV so origins can change without redeploy.
kv_namespaces = [
{ binding = "ROUTES", id = "f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6" }
]
[env.staging]
routes = [{ pattern = "staging.example.com/*", zone_name = "example.com" }]
vars = { ENVIRONMENT = "staging" }
[env.production]
routes = [{ pattern = "example.com/*", zone_name = "example.com" }]
vars = { ENVIRONMENT = "production" }
Binding a KV namespace lets you store the routing table as data rather than code, so updating an origin is a wrangler kv key put call instead of a redeploy. Read it inside the Worker with await env.ROUTES.get('/api/v2'). Keep the in-code routeMap as a fallback for when KV is empty or unreachable.
Expected output: wrangler deploy --dry-run --env staging prints the resolved bindings and route patterns without publishing, letting you catch a malformed zone_name or missing KV id before it reaches production.
Step 3 — Deploy to Staging, Then Production
Always deploy to staging first to isolate configuration drift and confirm the route binding attaches to the correct zone. Use the --env flag for isolated deployments.
# Stage first
wrangler deploy --minify --env staging
curl -I -s https://staging.example.com/api/v2/health | grep -Ei 'cf-ray|server'
# Promote to production once staging is verified
wrangler deploy --minify --env production
curl -I -s https://example.com/api/v2/health | grep -Ei 'cf-ray|server'
Expected output: each curl -I returns a cf-ray header (confirming the request traversed Cloudflare) and server: cloudflare. The presence of cf-ray is the single fastest signal that proxying is active and your Worker is in the path. If cf-ray is absent, proxying is off and the Worker is being bypassed.
Rollback procedure: if routing breaks post-deploy, immediately revert with wrangler rollback (which restores the previous deployment for that environment) or redeploy a known-good version by its ID. List versions with wrangler deployments list --env production and confirm the active one with wrangler deployments status.
Verification & Edge Diagnostics
Verify routing behavior, inspect which origin served the request, and confirm proxy status using CLI diagnostics and live log streaming. Validate against production DNS before declaring success.
# 1. Confirm the hostname is proxied (CNAME flatten to Cloudflare, not your origin IP)
dig +short example.com
# 2. Inspect status code and any redirect target for a routed path
curl -I -s -o /dev/null -w '%{http_code} %{redirect_url}\n' https://example.com/api/v2/users
# 3. Stream live Worker execution logs while you replay traffic
wrangler tail --env production --format pretty
Expected output: dig +short returns Cloudflare anycast addresses (typically in the 104.x / 172.x ranges) rather than your origin’s real IP — confirmation that the record is proxied. The curl call prints 200 for a healthy routed path. In the wrangler tail stream you should see one log line per request with the matched route and outcome, e.g. GET /api/v2/users - Ok @ 12:04:51. Watch the CPU time field; sustained values near your plan limit indicate the routing logic needs trimming.
Troubleshooting
Infinite redirect loops from misconfigured path rewriting
- Symptom:
5xxerrors, acf-cache-status: HITappearing on dynamic paths, and unexpectedly high egress. - Diagnosis: Overlapping prefix matches or a rewrite that targets a hostname still routed back through the same Worker, causing recursive edge fetches.
- Fix: Ensure rewritten origins point to hostnames that are NOT covered by the Worker’s
routespattern. Trace the chain withcurl -L -vand prefer exact prefix checks over loose substring matching.
Worker exceeds CPU limits (10ms free tier; higher on paid, configurable)
- Symptom:
502 Bad Gateway, dropped requests, and latency SLA degradation under load. - Diagnosis: Heavy synchronous work — large JSON parsing, complex regex, or blocking transforms — running inside the request path.
- Fix: Move large lookup tables into KV and read them lazily, push logging into
ctx.waitUntil(), and simplify pattern matching. Confirm the new CPU profile in the dashboard’s Workers analytics.
DNS proxy disabled (grey cloud) so the Worker never runs
- Symptom: Routing logic appears to do nothing, the origin IP is exposed, and
cf-rayis missing from responses. - Diagnosis: The DNS record is set to DNS-only mode, bypassing Cloudflare’s reverse proxy.
- Fix: Toggle the record to proxied (orange cloud), then confirm with
dig +short example.comthat it resolves to Cloudflare anycast IPs rather than your origin.
Route pattern attached but requests skip the Worker
- Symptom:
cf-raypresent, but no log lines inwrangler tailand original (un-rewritten) content is served. - Diagnosis: The
routespattern inwrangler.tomldoes not match the requested path, or a more specific Worker route shadows it. - Fix: Verify the pattern includes a trailing
/*, confirmzone_namematches the zone, and check the dashboard’s Workers Routes page for an overlapping higher-priority route.
Frequently Asked Questions
How do I route to different origins based on custom request headers?
Read the header with request.headers.get('X-Target-Origin'), validate it against an allowlist, build a new URL, and return fetch(new Request(targetUrl, request)). Never trust the raw header value as an origin without an allowlist, or you create an open proxy.
Can I bypass the Cloudflare cache during dynamic routing?
Yes. Pass cf: { cacheTtl: 0 } in the fetch options, or have the origin return Cache-Control: no-store. During testing, watch the cf-cache-status response header — DYNAMIC or BYPASS confirms the edge is not caching the routed response.
How do I debug routing failures in production without redeploying?
Stream live logs with wrangler tail --env production --format pretty and correlate the cf-ray value from a curl -I response with the matching log line. For deeper forensics, enable Logpush or query the GraphQL Analytics API for per-request error traces.
Why does request.cf.country sometimes return undefined?
The cf object is only populated when the request is proxied through Cloudflare’s edge. In wrangler dev local mode it can be absent, so always guard access with optional chaining and a default, e.g. request.cf?.country ?? 'US'.
Related
- Cloudflare Workers Routing
- Cloudflare Workers vs AWS Lambda@Edge for Request Routing
- JWT Validation at the Edge with Cloudflare Workers
Back to Cloudflare Workers Routing