AWS Builder Center
Origin Routing Playbook: DNS and Edge routing synergy

Origin Routing Playbook: DNS and Edge routing synergy

Learn how to combine Amazon CloudFront programmable routing with DNS for comprehensive traffic control.

This article was co-authored with  Dor Fibert , AWS Solutions Architect

Overview

Traditional web routing relies on DNS as the primary traffic director. When you access a domain, DNS resolvers determine where to send your traffic. However, DNS routing has fundamental limitations that can restrict your control when you need it most.
Modern applications require capabilities beyond what DNS alone can provide:
  • Real-time routing decisions
  • Per-request intelligence
  • Instant failover capabilities
The synergy between DNS routing and Amazon CloudFront edge routing creates a powerful paradigm for traffic management.
This playbook shows you how to architect a hybrid routing system that combines DNS for geographic optimization with CloudFront Functions for business logic routing.

Understanding DNS resolution in CDN architectures

Traditional DNS resolution flow

Before exploring dynamic routing, it's important to understand how DNS resolution works in a typical CDN setup. Consider a user accessing  api.example.com :
Traditional DNS resolution flow

Key insight: There are actually two separate DNS resolution hops in this flow:
  1. Client → CDN: User's device resolves  api.example.com  to get CloudFront PoP IP
  1. CDN → Origin: CloudFront resolves  origins.example.com  to select which origin server to use
This playbook focuses on the second hop - how CloudFront selects and routes to origin servers.

DNS routing patterns at the origin level

When CloudFront needs to resolve your origin domain ( origins.example.com ), DNS can use various routing policies:
Weighted routing: Distribute traffic by percentage
shell
origins.example.com 300 IN A 192.0.2.10 ; 70% weight
origins.example.com 300 IN A 198.51.100.20 ; 30% weight
Latency-based routing: Route to closest healthy origin
shell
origins.example.com 300 IN A 192.0.2.10 ; us-east-1 (latency-based)
origins.example.com 300 IN A 198.51.100.20 ; eu-west-1 (latency-based)
Geolocation routing: Route based on user's geographic location
shell
origins.example.com 300 IN A 192.0.2.10 ; North America
origins.example.com 300 IN A 198.51.100.20 ; Europe

DNS caching challenges

DNS routing decisions are made at resolution time and cached at multiple layers:
DNS caching challenges

This creates several challenges:
Delayed control: DNS routing changes affect only new DNS resolutions. Existing cached entries continue using previous routing for hours, preventing instant traffic redirection or emergency failover.
Cache inconsistency: You experience different routing behaviors based on when your DNS cache last refreshed. Some users reach your new origin while others remain on the previous one.
Limited context: DNS can't route based on request headers, user attributes, session state, or dynamic conditions.
Limited granularity: DNS operates at the host/domain level only. It cannot route based on URL paths ( /api/v1  vs  /api/v2 ), query parameters ( ?tenant=enterprise ), or HTTP methods (GET vs POST). All requests to  api.example.com  get the same DNS resolution regardless of the specific resource being requested.

Real-world scenarios

Consider these scenarios where DNS routing limitations impact your application:
  • Emergency failover: Your primary origin fails, but 70% of users remain routed there for hours due to DNS caching
  • A/B testing: You want to shift 10% more traffic to variant B, but DNS-based routing has fundamental issues: (1) DNS changes take hours to propagate inconsistently, (2) DNS distributes lookups, not actual requests - a single DNS lookup might serve thousands of users, making precise percentage control impossible, and (3) users can't maintain consistent test group assignment across sessions since DNS resolution varies by location and time
  • Session stickiness: Users lose their shopping cart when DNS routes them to a different origin mid-session
  • Bot management: AI crawlers overwhelm your primary origin, but you can't route them to dedicated infrastructure
  • Multi-tenant routing: While DNS can separate tenants using subdomains ( tenant-a.example.com ,  tenant-b.example.com ), this requires clients to know their tenant subdomain upfront. Applications often need dynamic tenant routing based on runtime context like query parameters ( ?tenant=example-corp ) or request headers ( x-tenant-id: anycompany-123 ) sent to a single domain ( api.example.com ). DNS operates at the domain level and can't inspect this request content to make routing decisions.

The solution: CloudFront Functions and DNS synergy

How CloudFront Functions transform origin selection

Traditional CDN routing relies entirely on DNS for origin selection (as shown in the DNS resolution flow above). CloudFront Functions introduce a programmable layer that can override DNS decisions in real-time:
Enhanced flow (CloudFront Functions + DNS):
Enhanced flow (CloudFront Functions + DNS):


The hybrid architecture advantage

This creates a powerful two-tier routing system:
Tier 1 - Business Logic (CloudFront Functions):
  • User segmentation and personalization
  • Session stickiness and state management
  • Security threat routing and bot management
  • A/B testing and canary deployments
  • Multi-tenant isolation
Tier 2 - DNS-Level Routing (DNS):
  • Latency-based routing within each cell
  • Geographic and geolocation-based routing
  • Weighted traffic distribution
  • Health checks and automatic failover
  • Load distribution across regions
  • Cost optimization by region

Architecture overview

The hybrid routing system works by having CloudFront Functions dynamically select which DNS domain to resolve, rather than using a single static origin domain:
Architecture overview 2 layer DNS setup

How the DNS resolution changes:
Instead of CloudFront always resolving the same  origins.example.com  domain, the CloudFront Function dynamically selects which domain to resolve based on request context:
  • Traditional: CloudFront → DNS resolve  origins.example.com  → Single origin pool
  • Hybrid: CloudFront → Function logic → DNS resolve  cell-premium.example.com  → Premium origin pool
Each cell domain ( cell-premium.example.com ,  cell-basic.example.com , etc.) has its own DNS configuration optimized for that specific use case - different regions, different infrastructure tiers, different failover policies.

Implementation: The CloudFront Function

Runtime requirements: The origin modification helpers ( updateRequestOrigin() ,  selectRequestOriginById() ,  createRequestOriginGroup() ) require  JavaScript runtime 2.0  and can be used in viewer request functions only. They come from the  cloudfront  module — every function snippet in this article assumes  import cf from 'cloudfront';  as the first line of the function code.
Here's the core routing logic that makes this possible:
JavaScript
// Implementation: CloudFront Function (JavaScript runtime 2.0, viewer request trigger)
import cf from 'cloudfront';

function handler(event) {
const request = event.request;
// Extract business context from request
const userSegment = request.headers['x-user-segment'] && request.headers['x-user-segment'].value;
const wafBotCategory = request.headers['x-waf-bot-category'] && request.headers['x-waf-bot-category'].value;
const wafThreatType = request.headers['x-waf-threat-type'] && request.headers['x-waf-threat-type'].value;
const tenantType = request.headers['x-tenant-type'] && request.headers['x-tenant-type'].value;
// Cell mapping for different user types
const cellMap = {
'__default__': 'cell-standard.example.com',
'premium': 'cell-premium.example.com',
'basic': 'cell-basic.example.com',
'enterprise': 'cell-enterprise.example.com',
'bot': 'bot-handler.example.com',
'security': 'security-isolated.example.com'
};
let targetCell;
// Business logic routing
if (wafThreatType === 'malicious' || wafBotCategory === 'scraping_framework') {
targetCell = wafThreatType === 'malicious' ? cellMap['security'] : cellMap['bot'];
} else if (tenantType === 'enterprise') {
targetCell = cellMap['enterprise'];
} else if (userSegment === 'premium') {
targetCell = cellMap['premium'];
} else if (userSegment === 'basic') {
targetCell = cellMap['basic'];
} else {
targetCell = cellMap['__default__'];
}
// Route to the selected cell
cf.updateRequestOrigin({ "domainName": targetCell });
return request;
}
Choosing the right origin helper:  updateRequestOrigin()  can point the request at any HTTP endpoint, even one not configured in your distribution — any property you don't specify is inherited from the assigned origin. If your target origins are already defined in the distribution,  cf.selectRequestOriginById("origin-id")  applies that origin's full configuration instead (and is the only way to route to VPC origins or mTLS-enabled origins, which  updateRequestOrigin()  can't target). For high availability,  cf.createRequestOriginGroup()  builds an origin group (primary + secondary with failover criteria) on the fly. Note that the CloudFront test console runs the function but doesn't simulate the origin change itself.

DNS configuration for geographic optimization

Each cell domain uses DNS latency-based routing for optimal performance:
shell
# Premium cell - global presence for high-value users
cell-premium.example.com 300 IN A 192.0.2.10 ; us-east-1 (latency-based)
cell-premium.example.com 300 IN A 198.51.100.20 ; eu-west-1 (latency-based)
cell-premium.example.com 300 IN A 203.0.113.30 ; ap-southeast-1 (latency-based)

# Basic cell - cost-optimized regions
cell-basic.example.com 300 IN A 192.0.2.40 ; us-west-2 (latency-based)
cell-basic.example.com 300 IN A 198.51.100.50 ; eu-central-1 (latency-based)

# Bot handling - dedicated infrastructure
bot-handler.example.com 300 IN A 203.0.113.60 ; specialized bot origin

# Security isolation - separate network
security-isolated.example.com 300 IN A 192.0.2.70 ; security-hardened origin

Live demo integration

Link to AWS Sample: CloudFront Programmable Origin Routing Demo
The sample includes an interactive React demo that visualizes:
  • Real-time session stickiness behavior
  • User-to-origin routing decisions
  • Comparison between sticky and non-sticky requests
  • Session tracking across multiple origins
Interactive demo showing how users maintain session state across requests


Use cases: beyond basic routing

Session stickiness for e-commerce

Challenge: Shopping cart data scattered across origins, users lose items mid-session.
Solution: Route users to consistent origins based on session identifiers.
JavaScript
// Implementation: CloudFront Function
// JavaScript has no built-in string hash, so bring your own.
// Any deterministic hash works - this is the classic djb2-style hash:
function hashCode(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) - hash + str.charCodeAt(i)) | 0; // keep it in 32 bits
}
return Math.abs(hash);
}

// Extract session from cookie or header
const sessionId = request.headers['x-session-id'] && request.headers['x-session-id'].value;
const originId = sessionId ? String(hashCode(sessionId) % 3) : '0';

// Route to consistent origin for this session
const sessionOriginMap = {
'0': 'cart-origin-0.example.com',
'1': 'cart-origin-1.example.com',
'2': 'cart-origin-2.example.com'
};

cf.updateRequestOrigin({ "domainName": sessionOriginMap[originId] });

Multi-tenant isolation

Challenge: Different tenants need isolated infrastructure for compliance and performance.
Solution: Route tenants to dedicated origins with proper isolation.
JavaScript
// Implementation: CloudFront Function
// Tenant-based routing
const tenantId = request.headers['x-tenant-id'] && request.headers['x-tenant-id'].value;

const tenantOriginMap = {
'anycompany': 'tenant-anycompany.example.com',
'example-corp': 'tenant-example-corp.example.com',
'standard': 'tenant-standard.example.com'
};

const targetOrigin = tenantOriginMap[tenantId] || tenantOriginMap['standard'];
cf.updateRequestOrigin({ "domainName": targetOrigin });

Bot and crawler management

Challenge: AI crawlers and bots overwhelm production infrastructure.
Solution: Route bots to specialized origins with rate limiting and caching.
JavaScript
// Implementation: CloudFront Function
// AWS WAF-enhanced bot routing
const wafBotCategory = request.headers['x-waf-bot-category'] && request.headers['x-waf-bot-category'].value;
const userAgent = request.headers['user-agent'] && request.headers['user-agent'].value;

let targetOrigin = 'production.example.com';

if (wafBotCategory === 'scraping_framework' || wafBotCategory === 'http_library') {
targetOrigin = 'bot-optimized.example.com';
} else if (userAgent && (userAgent.includes('GPTBot') || userAgent.includes('ChatGPT'))) {
targetOrigin = 'ai-crawler.example.com';
} else if (wafBotCategory === 'search_engine') {
targetOrigin = 'seo-optimized.example.com';
}

cf.updateRequestOrigin({ "domainName": targetOrigin });

Device-aware routing

Challenge: Mobile users need optimized experiences, smart TVs need streaming-optimized content.
Solution: Use CloudFront's computed headers for device detection.
JavaScript
// Implementation: CloudFront Function
// Device-aware routing using CloudFront computed headers
const isMobile = request.headers['cloudfront-is-mobile-viewer'] && request.headers['cloudfront-is-mobile-viewer'].value === 'true';
const isTablet = request.headers['cloudfront-is-tablet-viewer'] && request.headers['cloudfront-is-tablet-viewer'].value === 'true';
const isSmartTV = request.headers['cloudfront-is-smarttv-viewer'] && request.headers['cloudfront-is-smarttv-viewer'].value === 'true';
const country = request.headers['cloudfront-viewer-country'] && request.headers['cloudfront-viewer-country'].value;

let deviceOrigin;
if (isSmartTV) {
deviceOrigin = 'streaming-optimized.example.com';
} else if (isMobile || isTablet) {
deviceOrigin = `mobile-${country && country.toLowerCase() || 'global'}.example.com`;
} else {
deviceOrigin = `desktop-${country && country.toLowerCase() || 'global'}.example.com`;
}

cf.updateRequestOrigin({ "domainName": deviceOrigin });

Intelligent failover for POST requests

Challenge: CloudFront's native failover works for GET requests, but POST requests need special handling.
Solution: Two-phase failover pattern using GET health checks followed by targeted POST routing.
JavaScript
// Implementation: Client-side JavaScript
// Client-side failover pattern
const discoverHealthyOrigin = async () => {
try {
// Phase 1: Use GET request for health discovery
const response = await fetch('/api/health');
if (response.ok) {
const originHeader = response.headers.get('x-served-by-origin');
return originHeader || 'primary';
}
} catch (error) {
console.log('Primary path failed, trying backup');
}
// Phase 2: Try backup origin
try {
const backupResponse = await fetch('/api/health', {
headers: { 'x-origin-id': 'backup' }
});
if (backupResponse.ok) return 'backup';
} catch (error) {
console.log('Backup also failed');
}
return 'default';
};

// Use discovered healthy origin for POST
const submitOrder = async (orderData) => {
const healthyOrigin = await discoverHealthyOrigin();
const headers = { 'Content-Type': 'application/json' };
if (healthyOrigin !== 'default') {
headers['x-origin-id'] = healthyOrigin;
}
return fetch('/api/orders', {
method: 'POST',
headers,
body: JSON.stringify(orderData)
});
};
Which use case would you like to see as a complete AWS sample?
  • E-commerce session stickiness (already implemented -  see demo  ) 
  • POST request failover patterns
  • Multi-tenant isolation
  • Device-aware routing
  • Bot management and AI crawler handling

Production considerations

DNS TTL strategy

Alias vs Non-Alias Records Trade-offs:
Alias Records: No DNS query costs but no TTL control - Amazon Route 53 sets TTL automatically. Best for stable, cost-optimized origins.
Non-Alias Records: Full TTL control (including zero TTL for real-time updates) but charged per DNS query. Higher costs and latency with lower TTLs, but enables dynamic DNS changes.
TTL Guidelines:
  • Zero TTL (0s): Real-time DNS updates, high cost/latency - emergency failover only
  • Short TTL (60-300s): Quick changes, moderate cost - development/testing environments
  • Long TTL (3600s+): Cost-effective, stable - production workloads

Security: production header patterns

⚠️Critical: Don't use predictable headers in production. The demo uses simple headers for education.
Demo (Educational Only):
JavaScript
// Implementation: CloudFront Function
// ❌ Avoid in production - too predictable
const originId = request.headers['x-origin-id'] && request.headers['x-origin-id'].value; // '0', '1', '2'
Suggested approach for production environments:
JavaScript
// Implementation: CloudFront Function
// ✅ Production approach - obfuscated and secure
const routingToken = request.headers['x-rt'] && request.headers['x-rt'].value; // encrypted token
const sessionHash = request.headers['x-sh'] && request.headers['x-sh'].value; // hashed session ID

// Use encrypted/hashed values
const originMapping = {
'a7f3k9m2': 'origin-primary',
'b8g4l0n3': 'origin-secondary',
'c9h5m1o4': 'origin-tertiary'
};

const targetOrigin = originMapping[routingToken] || 'default-origin.example.com';

Cookie-based session management

For persistent session stickiness across browser sessions:
JavaScript
// Implementation: Origin Server
// Server-side: Set secure session cookie
res.cookie('session-origin', originId, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 24 * 60 * 60 * 1000 // 24 hours
});

// Implementation: CloudFront Function
// CloudFront function: Read from cookie
// Cookies are not part of request.headers - CloudFront Functions
// expose them separately in the request.cookies object
const sessionOrigin = request.cookies['session-origin'] && request.cookies['session-origin'].value;

if (sessionOrigin && isValidOrigin(sessionOrigin)) {
cf.updateRequestOrigin({ "domainName": sessionOrigin });
}

Key value store for dynamic updates

Use CloudFront key value store for runtime routing changes without code deployments:
The key value store is bound to the function by association (you associate one store with the function in the CloudFront console, CLI, or IaC), so  cf.kvs()  takes no arguments and each function can use a single store. Grab the handle once at the top of the function code, and note that  get()  throws when a key doesn't exist — wrap each lookup in  try / catch :
JavaScript
// Implementation: CloudFront Function (JavaScript runtime 2.0, viewer request trigger)
import cf from 'cloudfront';

// The store is selected by its association with this function - kvs() takes no arguments
const kvsHandle = cf.kvs();

async function handler(event) {
const request = event.request;
const userId = request.headers['x-user-id'] && request.headers['x-user-id'].value;
try {
// Check for user-specific routing override
const userOverride = await kvsHandle.get(`user-${userId}`, { format: 'string' });
cf.updateRequestOrigin({ "domainName": userOverride });
return request;
} catch (error) {
// Key not found - fall through to default routing
}
try {
// Fall back to the default origin stored in KVS
const defaultOrigin = await kvsHandle.get('default-origin', { format: 'string' });
cf.updateRequestOrigin({ "domainName": defaultOrigin });
} catch (error) {
// Key value store lookup failed, use hardcoded fallback
cf.updateRequestOrigin({ "domainName": 'fallback-origin.example.com' });
}
return request;
}

Summary: the power of hybrid routing

The synergy between DNS and CloudFront edge routing creates a powerful new paradigm for traffic management:
DNS handles: Geographic optimization, latency-based routing, basic health checks CloudFront Functions handle: Business logic, session stickiness, real-time decisions, security routing
Key benefits:
  • Instant control: Changes propagate in seconds, not hours
  • Per-request intelligence: Route based on headers, user context, and dynamic conditions
  • Business logic integration: Implement complex routing rules in JavaScript
  • Session preservation: Maintain user state across requests
  • Security enhancement: Route threats to isolated environments
  • Cost optimization: Direct traffic to appropriate infrastructure tiers
Best practices:
  1. Use CloudFront Functions for business logic routing
  1. Use DNS for geographic and latency optimization within each cell
  1. Implement secure, obfuscated headers in production
  1. Monitor routing decisions with custom metrics
  1. Use key value store for dynamic routing updates without deployments
  1. Plan for failover scenarios with health check patterns
The future of web routing is programmable, intelligent, and instant. By combining the geographic optimization of DNS with the business logic capabilities of CloudFront Functions, you can build routing systems that adapt in real-time to your users' needs.
Ready to implement programmable origin routing? Start with the  AWS CloudFront Programmable Origin Routing Demo   and build your own hybrid routing architecture. 


For questions about implementing hybrid DNS and edge routing solutions, or to share your specific routing challenges, please comment on this post.
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article