
Origin Routing Playbook: DNS and Edge routing synergy
Learn how to combine Amazon CloudFront programmable routing with DNS for comprehensive traffic control.
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 :
Key insight: There are actually two separate DNS resolution hops in this flow:
- Client → CDN: User's device resolves
api.example.comto get CloudFront PoP IP
- CDN → Origin: CloudFront resolves
origins.example.comto 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% weightorigins.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 Americaorigins.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:

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):

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:

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 thecloudfrontmodule — every function snippet in this article assumesimport 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 requestconst 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 typesconst 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 routingif (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 cellcf.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 userscell-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 regionscell-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 infrastructurebot-handler.example.com 300 IN A 203.0.113.60 ; specialized bot origin# Security isolation - separate networksecurity-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 headerconst 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 sessionconst 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 routingconst 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 routingconst 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 headersconst 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 patternconst discoverHealthyOrigin = async () => {try {// Phase 1: Use GET request for health discoveryconst 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 origintry {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 POSTconst 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?
- POST request failover patterns
- Multi-tenant isolation
- Device-aware routing
- Bot management and AI crawler handling
- A/B testing patterns with CloudFront (see the dedicated guide: Application Performance A/B Testing )
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 predictableconst 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 secureconst routingToken = request.headers['x-rt'] && request.headers['x-rt'].value; // encrypted tokenconst sessionHash = request.headers['x-sh'] && request.headers['x-sh'].value; // hashed session ID// Use encrypted/hashed valuesconst 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 cookieres.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 objectconst 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 argumentsconst 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 overrideconst 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 KVSconst defaultOrigin = await kvsHandle.get('default-origin', { format: 'string' });cf.updateRequestOrigin({ "domainName": defaultOrigin });} catch (error) {// Key value store lookup failed, use hardcoded fallbackcf.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:
- Use CloudFront Functions for business logic routing
- Use DNS for geographic and latency optimization within each cell
- Implement secure, obfuscated headers in production
- Monitor routing decisions with custom metrics
- Use key value store for dynamic routing updates without deployments
- 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.
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article