A routing policy decides which answer Route 53 returns when several records share a name. That is how one hostname can serve different users differently, by region, by health, or by a weighting you control.
| Policy | Answers based on |
|---|---|
| Simple | One record, no logic |
| Weighted | A proportion you set |
| Latency | Lowest measured latency to the client |
| Failover | Health of a designated primary |
| Geolocation | Where the client appears to be |
| Geoproximity | Distance to the resource, with a bias |
| Multivalue | Up to eight healthy records, at random |
Any policy other than simple requires a SetIdentifier. A unique label distinguishing records that share a name and type. Omitting it is the first error everyone hits:
InvalidChangeBatch: RRSet of type A with DNS name api.example.com. is not
permitted because a conflicting RRSet exists
Route 53 is telling you it cannot hold two records with the same name and type unless each carries a distinct identifier.
Splits traffic by proportion. The workhorse for canaries and blue-green cutovers:
aws route53 change-resource-record-sets --hosted-zone-id "$ZONE" --change-batch '{
"Changes": [
{"Action": "UPSERT", "ResourceRecordSet": {
"Name": "api.example.com", "Type": "A", "TTL": 60,
"SetIdentifier": "blue", "Weight": 90,
"ResourceRecords": [{"Value": "203.0.113.10"}]
}},
{"Action": "UPSERT", "ResourceRecordSet": {
"Name": "api.example.com", "Type": "A", "TTL": 60,
"SetIdentifier": "green", "Weight": 10,
"ResourceRecords": [{"Value": "203.0.113.20"}]
}}
]
}'Weights are relative, not percentages. They are divided by the total, so 90 and 10 gives 90%, and so does 900 and 100. Weight 0 removes a record from consideration unless every record is 0, in which case all are eligible. Setting one side to 0 is the clean way to complete a cutover without deleting anything.
Two caveats worth knowing before you trust a 10% canary.
The split is per DNS query, not per request. A client that resolves once and keeps a connection open sends everything to whichever answer it got, so a small sample can skew badly. Keep TTLs low and give the canary time.
Resolvers cache and clients pool. Real distribution converges on the weights over time but is genuinely noisy at short timescales, which is why weighted DNS is a reasonable canary mechanism and a poor traffic-shaping one. If you need precise percentages, do it at the load balancer with weighted target groups.
An active-passive pair driven by a health check:
{"Action": "UPSERT", "ResourceRecordSet": {
"Name": "www.example.com", "Type": "A", "TTL": 60,
"SetIdentifier": "primary", "Failover": "PRIMARY",
"HealthCheckId": "abcd1234-5678-90ef-ghij-klmnopqrstuv",
"AliasTarget": {"HostedZoneId": "Z35SXDOTRQ7X7K",
"DNSName": "prod-alb-123.eu-west-1.elb.amazonaws.com",
"EvaluateTargetHealth": true}
}}{"Action": "UPSERT", "ResourceRecordSet": {
"Name": "www.example.com", "Type": "A", "TTL": 60,
"SetIdentifier": "secondary", "Failover": "SECONDARY",
"AliasTarget": {"HostedZoneId": "Z3AQBSTGFYJSTF",
"DNSName": "s3-website-eu-west-1.amazonaws.com",
"EvaluateTargetHealth": false}
}}The primary must have a health check: either an explicit HealthCheckId or EvaluateTargetHealth on an alias. Without one it is always considered healthy and failover never occurs, which is a configuration that looks complete and does nothing.
The secondary does not need one, and a static S3 maintenance page is a good choice for it: it will be serving precisely when everything else is broken, so it should depend on as little as possible.
Remember the timing from the Route 53 overview: detection is around 90 seconds and cached TTLs add their own delay.
Routes each client to the region with the lowest measured latency from their resolver:
{"Name": "api.example.com", "Type": "A", "SetIdentifier": "eu-west-1",
"Region": "eu-west-1", "TTL": 60, "ResourceRecords": [{"Value": "203.0.113.10"}]}Note Region is an AWS region name, and Route 53 uses its own latency measurements rather than geographic distance, which is usually better, since network paths do not follow maps.
The subtlety: latency is measured to the resolver, not the end user. A client using a distant public resolver can be routed to a distant region. Usually minor, and occasionally the explanation for a puzzling report.
Routes by where the client appears to be, which is about compliance and content, not performance:
{"Name": "www.example.com", "Type": "A", "SetIdentifier": "eu",
"GeoLocation": {"ContinentCode": "EU"}, "TTL": 300, â¦}
{"Name": "www.example.com", "Type": "A", "SetIdentifier": "de",
"GeoLocation": {"CountryCode": "DE"}, "TTL": 300, â¦}
{"Name": "www.example.com", "Type": "A", "SetIdentifier": "default",
"GeoLocation": {"CountryCode": "*"}, "TTL": 300, â¦}Most specific wins: a German client matches the DE record over the EU one.
Always create a * default record. Without it, a client whose location Route 53 cannot determine receives no answer at all: not a fallback, a NODATA response. This is the most common geolocation mistake and it fails for exactly the users you cannot easily reproduce.
Use geolocation for data residency and licensing. For performance, use latency routing.
Routes by geographic distance, with a bias that expands or shrinks a resource's catchment area:
{"Name": "api.example.com", "Type": "A", "SetIdentifier": "eu-central",
"GeoProximityLocation": {"AWSRegion": "eu-central-1", "Bias": 40}, â¦}A positive bias pulls in traffic from further away; a negative one pushes it out. That is the feature. It lets you shift load toward a region with spare capacity without changing anything else. It is the only policy that needs traffic flow policies in the console for the visual editor, though the API works directly.
Returns up to eight healthy records at random, with health checking that simple routing lacks:
{"Name": "api.example.com", "Type": "A", "TTL": 60,
"SetIdentifier": "host-1", "MultiValueAnswer": true,
"HealthCheckId": "â¦", "ResourceRecords": [{"Value": "203.0.113.10"}]}The difference from a simple record with multiple values is exactly that health awareness: a simple multi-value record keeps returning a dead address, while multivalue omits records whose health check is failing.
It is not a load balancer. There is no connection distribution or session handling, and clients choose from the list themselves. It suits many equivalent endpoints where a real load balancer is unnecessary.
| Goal | Policy |
|---|---|
| Canary or blue-green | Weighted |
| Disaster recovery | Failover |
| Fastest response globally | Latency |
| Data residency or licensing | Geolocation |
| Shift load between regions | Geoproximity |
| Several equivalent hosts, health-aware | Multivalue |
Policies can be nested through traffic flow, latency routing to pick a region, then weighted within it for a canary, which is where geoproximity's visual editor earns its place.
InvalidChangeBatch: RRSet of type A with DNS name api.example.com. is not
permitted because a conflicting RRSet exists
Missing or duplicate SetIdentifier.
InvalidChangeBatch: Health check must be specified for failover records
The primary needs a health check or EvaluateTargetHealth.
Geolocation returns nothing for some users. No CountryCode: "*" default record.
Weighted split does not match the weights. Caching and connection reuse. Expect convergence over time, not precision.
| Policy | Required fields |
|---|---|
| Weighted | SetIdentifier, Weight |
| Failover | SetIdentifier, Failover, health check on primary |
| Latency | SetIdentifier, Region |
| Geolocation | SetIdentifier, GeoLocation, plus a * default |
| Geoproximity | SetIdentifier, GeoProximityLocation, Bias |
| Multivalue | SetIdentifier, MultiValueAnswer: true |
SetIdentifier.* default returns nothing to unrecognised clients.<MCQExercise exercise={{ id: 'aws-r53-policy-quiz1', title: 'Failover that never happens', description: 'A failover record set has a PRIMARY pointing at an ALB and a SECONDARY pointing at an S3 maintenance page. The primary ALB is down, but traffic keeps going to it. Both records have SetIdentifiers. What is missing?', options: [ { id: 'a', text: 'A health check on the primary, without HealthCheckId or EvaluateTargetHealth it is always treated as healthy, so failover never triggers' }, { id: 'b', text: 'The secondary record needs its own health check before it can receive traffic' }, { id: 'c', text: 'Failover routing requires both records to have equal weights' }, { id: 'd', text: 'S3 website endpoints cannot be used as a failover secondary' }, ], correctId: 'a', explanation: 'Route 53 only fails over when it believes the primary is unhealthy, and with no health check attached it has no basis for that belief, so the configuration looks complete and does nothing. Attach a HealthCheckId, or set EvaluateTargetHealth on the alias so the ALB's own target health drives the decision. The secondary deliberately needs no health check, and pointing it at a static S3 page is good practice precisely because it has to work when everything else has failed.', wrongExplanations: { b: 'The secondary is the fallback of last resort and does not require a health check; adding one risks having no healthy answer at all.', c: 'Weights belong to weighted routing. Failover uses the PRIMARY and SECONDARY designations.', d: 'An S3 website endpoint is a common and recommended failover target for a maintenance page.', }, }} />
<MCQExercise exercise={{ id: 'aws-r53-policy-quiz2', title: 'Users who get no answer at all', description: 'Geolocation routing sends EU clients to one region and US clients to another. A small number of users report the domain not resolving at all, rather than resolving to the wrong place. Why?', options: [ { id: 'a', text: 'There is no default record with CountryCode "", so clients whose location cannot be determined receive no answer rather than a fallback' }, { id: 'b', text: 'Geolocation routing requires a health check on every record' }, { id: 'c', text: 'Those users are behind IPv6, which geolocation routing does not support' }, { id: 'd', text: 'Their DNS TTL has expired and needs manual refresh' }, ], correctId: 'a', explanation: 'Geolocation matches a client's apparent location against the records you have defined, and if none matches it returns NODATA, no answer, not a nearest guess. Clients behind VPNs, corporate resolvers or unmapped address ranges land in that gap, which is why only a minority are affected and why it is hard to reproduce. Always create a record with GeoLocation CountryCode set to "" as the catch-all. The same applies to geoproximity, where a resource should cover locations no other record claims.', wrongExplanations: { b: 'Health checks are optional for geolocation records and would not address an unmatched location.', c: 'Geolocation works over IPv6; address family is not the issue.', d: 'An expired TTL causes a fresh lookup, which would return the same NODATA response.', }, }} />
Weighted routing. Create two records with the same name and type, distinct SetIdentifier values, and weights in the ratio you want: 95 and 5 to start. Shift the weights gradually and set one to 0 to complete the cutover, which removes it from consideration without deleting anything. Keep TTLs at 60 so changes take effect quickly. Bear in mind that weights apply per DNS query rather than per request, so short observations are noisy; for precise percentages use weighted target groups on an ALB instead.
Because weighting applies to DNS answers, not requests. A resolver caches the answer for the TTL and serves it to everyone behind it, and a client that resolves once then reuses a connection sends all its traffic to whichever endpoint it got. Distribution converges on the configured ratio over time but is genuinely uneven at short timescales, especially with large corporate resolvers. Lower TTLs help. For accurate traffic splitting, do it at the load balancer.
It distinguishes records that share the same name and type, which is what makes any non-simple routing policy possible. Route 53 otherwise refuses to store two A records for api.example.com. The value is an arbitrary label, so use something meaningful like blue, green, eu-west-1 or primary. Omitting it produces a "conflicting RRSet exists" error, which is the most common first error when moving from simple to weighted or failover routing.
When you have several equivalent endpoints and want unhealthy ones removed from the answers. A simple record holding multiple values returns all of them regardless of state, so a dead address keeps being handed out; multivalue attaches a health check per record and omits the failing ones, returning up to eight at random. It is not a load balancer. There is no connection distribution and clients pick from the list themselves, but it is a cheap improvement over round-robin A records.
It expands or contracts the geographic area a resource serves. A positive bias draws traffic from further away, a negative one pushes nearby traffic to another resource, so you can shift load toward a region with spare capacity, or away from one under pressure, without touching the application. That gradual adjustment is the distinguishing feature versus geolocation, which assigns whole countries or continents. Geoproximity is also where the traffic flow visual editor in the console is genuinely useful.
Learn DynamoDB if you know SQL: partition and sort keys, designing for access patterns first, avoiding hot partitions, conditional writes, streams and TTL.
Create a DynamoDB table with the AWS CLI. Choose the key schema you cannot change later, enable deletion protection and PITR, and take on-demand backups.
Learn DynamoDB Query vs Scan, why filter expressions do not cut cost, how GSIs differ from LSIs, and how to paginate results and use sparse indexes.