AWS Route 53 Routing Policies: The Complete Guide

Bits Lovers
Written by Bits Lovers on
AWS Route 53 Routing Policies: The Complete Guide

Most engineers use Route 53 for one thing: create an A record pointing to a load balancer and move on. But Route 53 has seven routing policies, each solving a different traffic distribution problem. Latency-based routing automatically sends users to the nearest healthy region. Weighted routing enables canary deployments without touching your load balancer config. Failover routing handles active-passive DR without any application changes. Getting comfortable with all of them changes how you approach global availability and blue/green deployments.

This guide covers every routing policy with practical examples, health check configuration, and the scenarios where each one earns its keep.

Alias Records and Why They Matter

Before routing policies: alias records. An alias record points to an AWS resource — ALB, CloudFront distribution, S3 bucket website endpoint, API Gateway — and behaves like a CNAME but has two advantages over regular CNAMEs.

First, alias records at the zone apex (naked domain: example.com without a subdomain) work. Regular CNAMEs can’t be at the zone apex. Second, queries to alias records are free — Route 53 doesn’t charge for queries that resolve to AWS resources. For a high-traffic domain, the cost difference between an alias and a CNAME is real.

# Create an alias record pointing to an ALB
aws route53 change-resource-record-sets \
  --hosted-zone-id Z1234567890 \
  --change-batch '{
    "Changes": [{
      "Action": "CREATE",
      "ResourceRecordSet": {
        "Name": "api.example.com",
        "Type": "A",
        "AliasTarget": {
          "HostedZoneId": "Z35SXDOTRQ7X7K",
          "DNSName": "my-alb-123456.us-east-1.elb.amazonaws.com",
          "EvaluateTargetHealth": true
        }
      }
    }]
  }'

EvaluateTargetHealth: true makes Route 53 check whether the ALB itself is healthy before returning it. If the ALB has no healthy targets, Route 53 treats the alias record as unhealthy and can fail over to another record.

Simple Routing

One record, one answer. Route 53 returns the value and every DNS client gets the same response. Use it for internal-only services, development environments, or any service that doesn’t need distribution or failover:

aws route53 change-resource-record-sets \
  --hosted-zone-id Z1234567890 \
  --change-batch '{
    "Changes": [{
      "Action": "CREATE",
      "ResourceRecordSet": {
        "Name": "internal-api.example.com",
        "Type": "A",
        "TTL": 300,
        "ResourceRecords": [
          {"Value": "10.0.1.50"}
        ]
      }
    }]
  }'

Simple records can have multiple values — Route 53 returns all of them in a shuffled order and the client picks one (DNS round-robin). It’s not load balancing in any meaningful sense since there’s no health checking, but it provides basic distribution across a set of servers.

Weighted Routing

Assign weights to records with the same name. Route 53 distributes queries proportionally. The canonical use case is canary deployments: send 5% of traffic to the new version, watch for errors, then shift to 50%, then 100%:

# Blue version (95% of traffic)
aws route53 change-resource-record-sets \
  --hosted-zone-id Z1234567890 \
  --change-batch '{
    "Changes": [{
      "Action": "CREATE",
      "ResourceRecordSet": {
        "Name": "api.example.com",
        "Type": "A",
        "SetIdentifier": "blue",
        "Weight": 95,
        "AliasTarget": {
          "HostedZoneId": "Z35SXDOTRQ7X7K",
          "DNSName": "blue-alb.us-east-1.elb.amazonaws.com",
          "EvaluateTargetHealth": true
        }
      }
    }]
  }'

# Green version (5% of traffic)
aws route53 change-resource-record-sets \
  --hosted-zone-id Z1234567890 \
  --change-batch '{
    "Changes": [{
      "Action": "CREATE",
      "ResourceRecordSet": {
        "Name": "api.example.com",
        "Type": "A",
        "SetIdentifier": "green",
        "Weight": 5,
        "AliasTarget": {
          "HostedZoneId": "Z35SXDOTRQ7X7K",
          "DNSName": "green-alb.us-east-1.elb.amazonaws.com",
          "EvaluateTargetHealth": true
        }
      }
    }]
  }'

Weights are relative, not percentages. Weight 95 and 5 gives the same distribution as 19 and 1. Setting a record’s weight to 0 takes it out of rotation without deleting it — useful for quickly pulling the canary during an incident.

The DNS TTL means traffic doesn’t shift instantaneously when you update weights. Clients cache DNS responses for the TTL duration. Set TTL to 60 seconds for records used in blue/green deployments so you can shift traffic quickly.

Latency-Based Routing

Route 53 measures latency from different AWS regions to the requesting DNS resolver and returns the record from the region with the lowest measured latency. This is not geographic routing — it’s actual measured latency. A user in South America might get routed to us-east-1 if that genuinely has lower latency than sa-east-1 from their resolver’s perspective.

# Create latency records for each region
for REGION in us-east-1 eu-west-1 ap-southeast-1; do
  aws route53 change-resource-record-sets \
    --hosted-zone-id Z1234567890 \
    --change-batch "{
      \"Changes\": [{
        \"Action\": \"CREATE\",
        \"ResourceRecordSet\": {
          \"Name\": \"api.example.com\",
          \"Type\": \"A\",
          \"SetIdentifier\": \"$REGION\",
          \"Region\": \"$REGION\",
          \"AliasTarget\": {
            \"HostedZoneId\": \"ZONE_ID_FOR_$REGION\",
            \"DNSName\": \"alb-$REGION.elb.amazonaws.com\",
            \"EvaluateTargetHealth\": true
          }
        }
      }]
    }"
done

Always combine latency routing with health checks via EvaluateTargetHealth. Without it, Route 53 routes to the lowest-latency region even if that region’s ALB is down — clients get routed to a broken endpoint.

Failover Routing

Active-passive setup: Route 53 returns the primary record unless the primary health check fails, then returns the secondary. The primary must have a health check attached:

# Create health check for primary endpoint
HEALTH_CHECK_ID=$(aws route53 create-health-check \
  --caller-reference "primary-$(date +%s)" \
  --health-check-config '{
    "IPAddress": "203.0.113.1",
    "Port": 443,
    "Type": "HTTPS",
    "ResourcePath": "/health",
    "FullyQualifiedDomainName": "api.example.com",
    "RequestInterval": 30,
    "FailureThreshold": 3,
    "EnableSNI": true
  }' \
  --query 'HealthCheck.Id' --output text)

# Primary record
aws route53 change-resource-record-sets \
  --hosted-zone-id Z1234567890 \
  --change-batch "{
    \"Changes\": [{
      \"Action\": \"CREATE\",
      \"ResourceRecordSet\": {
        \"Name\": \"api.example.com\",
        \"Type\": \"A\",
        \"SetIdentifier\": \"primary\",
        \"Failover\": \"PRIMARY\",
        \"HealthCheckId\": \"$HEALTH_CHECK_ID\",
        \"AliasTarget\": {
          \"HostedZoneId\": \"Z35SXDOTRQ7X7K\",
          \"DNSName\": \"primary-alb.us-east-1.elb.amazonaws.com\",
          \"EvaluateTargetHealth\": true
        }
      }
    }]
  }"

# Secondary record (no health check required — returns if primary fails)
aws route53 change-resource-record-sets \
  --hosted-zone-id Z1234567890 \
  --change-batch '{
    "Changes": [{
      "Action": "CREATE",
      "ResourceRecordSet": {
        "Name": "api.example.com",
        "Type": "A",
        "SetIdentifier": "secondary",
        "Failover": "SECONDARY",
        "AliasTarget": {
          "HostedZoneId": "Z35SXDOTRQ7X7K",
          "DNSName": "dr-alb.us-west-2.elb.amazonaws.com",
          "EvaluateTargetHealth": true
        }
      }
    }]
  }'

Health check failure detection: Route 53 health checkers in 15+ locations probe your endpoint every 10 or 30 seconds. Failover triggers when at least 18% of health checkers report failure (roughly 3 of 15 locations). Time to failover: with 30-second intervals and a failure threshold of 3, worst case is 90 seconds. Switch to 10-second intervals if you need faster failover — it costs $1/month per health check vs $0.50/month for 30-second intervals.

Geolocation Routing

Route traffic based on the geographic location of the DNS query origin. Unlike latency routing, this is explicit location-based routing regardless of measured latency:

# EU users go to eu-west-1 (GDPR compliance)
aws route53 change-resource-record-sets \
  --hosted-zone-id Z1234567890 \
  --change-batch '{
    "Changes": [{
      "Action": "CREATE",
      "ResourceRecordSet": {
        "Name": "api.example.com",
        "Type": "A",
        "SetIdentifier": "eu",
        "GeoLocation": {
          "ContinentCode": "EU"
        },
        "AliasTarget": {
          "HostedZoneId": "EU_ZONE_ID",
          "DNSName": "eu-alb.eu-west-1.elb.amazonaws.com",
          "EvaluateTargetHealth": true
        }
      }
    }]
  }'

# Default for everything else
aws route53 change-resource-record-sets \
  --hosted-zone-id Z1234567890 \
  --change-batch '{
    "Changes": [{
      "Action": "CREATE",
      "ResourceRecordSet": {
        "Name": "api.example.com",
        "Type": "A",
        "SetIdentifier": "default",
        "GeoLocation": {
          "CountryCode": "*"
        },
        "AliasTarget": {
          "HostedZoneId": "US_ZONE_ID",
          "DNSName": "us-alb.us-east-1.elb.amazonaws.com",
          "EvaluateTargetHealth": true
        }
      }
    }]
  }'

Always create a default record (CountryCode: *) or queries from locations not matched by any specific rule return SERVFAIL. The default record is the catch-all.

Geolocation routing is the right tool for compliance-driven routing — GDPR requiring EU data to stay in EU, or regional licensing restrictions. For performance optimization, use latency-based routing instead.

Geoproximity Routing

Like geolocation but with a bias slider. Route to the geographically nearest endpoint, then expand or shrink each endpoint’s effective coverage area using a bias value from -99 to +99. Positive bias expands the region; negative bias shrinks it:

# Route to us-east-1 with bias of +25 (attracts more traffic from neighboring regions)
aws route53 change-resource-record-sets \
  --hosted-zone-id Z1234567890 \
  --change-batch '{
    "Changes": [{
      "Action": "CREATE",
      "ResourceRecordSet": {
        "Name": "api.example.com",
        "Type": "A",
        "SetIdentifier": "us-east-expanded",
        "GeoProximityLocation": {
          "AWSRegion": "us-east-1",
          "Bias": 25
        },
        "AliasTarget": {
          "HostedZoneId": "US_ZONE_ID",
          "DNSName": "us-alb.us-east-1.elb.amazonaws.com",
          "EvaluateTargetHealth": true
        }
      }
    }]
  }'

Geoproximity routing requires Traffic Flow — Route 53’s visual policy editor. The CLI representation uses the GeoProximityLocation key. This policy is useful when you have uneven regional capacity and want to shift the geographic boundaries of routing without building complex geolocation rules.

Calculated Health Checks

For complex applications, a single endpoint health check isn’t enough. Calculated health checks combine multiple child health checks using AND, OR, or minimum-threshold logic:

# Create calculated health check: healthy if at least 2 of 3 children are healthy
CALC_CHECK=$(aws route53 create-health-check \
  --caller-reference "calculated-$(date +%s)" \
  --health-check-config "{
    \"Type\": \"CALCULATED\",
    \"HealthThreshold\": 2,
    \"ChildHealthChecks\": [
      \"$DB_HEALTH_CHECK\",
      \"$API_HEALTH_CHECK\",
      \"$CACHE_HEALTH_CHECK\"
    ]
  }" \
  --query 'HealthCheck.Id' --output text)

Use calculated health checks when your application’s availability depends on multiple components. A calculated check on the Route 53 record fails over only when enough components are unhealthy — prevents false failovers from transient single-component hiccups.

Private Hosted Zones

Private hosted zones answer DNS queries only from within specified VPCs. Useful for service discovery within a VPC without exposing DNS to the public internet:

# Create private hosted zone
aws route53 create-hosted-zone \
  --name internal.example.com \
  --caller-reference "private-$(date +%s)" \
  --hosted-zone-config '{"PrivateZone": true}' \
  --vpc '{"VPCRegion":"us-east-1","VPCId":"vpc-0abc123"}'

# Associate with additional VPCs
aws route53 associate-vpc-with-hosted-zone \
  --hosted-zone-id Z1234567890 \
  --vpc '{"VPCRegion":"us-east-1","VPCId":"vpc-0def456"}'

Private hosted zones are the standard approach for internal service discovery in AWS. They’re also what enables SSM Session Manager to resolve SSM endpoints through VPC endpoints rather than over the public internet when you enable private DNS on the endpoint.

Pricing

Route 53 pricing: $0.50 per hosted zone per month (first 25 zones) plus query charges. Standard queries (simple, failover): $0.40 per million. Latency, weighted, geolocation, geoproximity: $0.60 per million. Health checks: $0.50/month for standard, $1.00/month for fast (10-second) interval.

A typical production setup — one hosted zone, 50 million queries/month, three health checks — costs about $21/month. For global applications using latency routing, that scales to $30/month at the same query volume. The cost is almost never the constraint when choosing routing policies.

For applications distributed across multiple AWS regions, the combination of latency routing with health checks is the baseline. Add failover routing for DR, weighted routing for blue/green deployments, and geolocation only when compliance requires explicit location-based routing. The AWS VPC design patterns guide covers how DNS integrates with multi-VPC architectures built on Transit Gateway.

Bits Lovers

Bits Lovers

Professional writer and blogger. Focus on Cloud Computing.

Comments

comments powered by Disqus