AWS Route 53
DNS on AWS. Public and private hosted zones, the delegation people forget, alias records, health checks, and why TTL decides failover speed.
Amazon Route 53
Route 53 is AWS's DNS service, and it does three separable jobs: registering domains, hosting zones that answer queries, and health-checking endpoints so DNS can route around failure.
Route 53: DNS Resolution
Health checks automatically remove unhealthy endpoints from DNS responses
Most confusion comes from mixing up registration and hosting. You can register a domain here and host it elsewhere, or register it elsewhere and host it here, which is the more common arrangement and the one that needs the delegation step below.
Hosted zones
aws route53 create-hosted-zone --name example.com \
--caller-reference "$(date +%s)" \
--hosted-zone-config Comment="production"
aws route53 get-hosted-zone --id Z1ABCDEFGHIJKL \
--query 'DelegationSet.NameServers'
[
"ns-123.awsdns-45.com",
"ns-678.awsdns-90.net",
"ns-234.awsdns-12.org",
"ns-567.awsdns-34.co.uk"
]
Those four name servers are the zone's answer to the world, and nothing works until your registrar points at them. If the domain is registered outside AWS, update the name servers at that registrar. If it is registered in Route 53, this is done for you.
Each hosted zone gets its own unique name server set. Deleting and recreating a zone produces four different names, so an accidental recreate breaks the domain until the registrar is updated again. There is also a monthly charge per hosted zone, which is why duplicate zones for the same domain are worth spotting early:
aws route53 list-hosted-zones --query 'HostedZones[].[Name,Id,ResourceRecordSetCount]' --output table
Two zones for the same domain is a genuinely common cause of "some records work and some do not", only one is authoritative, and you are editing the other.
Alias records
Route 53's most useful non-standard feature. A CNAME cannot exist at a zone apex, which is a DNS protocol constraint rather than an AWS one, so example.com cannot CNAME to a load balancer. An alias record solves it:
aws route53 change-resource-record-sets --hosted-zone-id Z1ABCDEFGHIJKL --change-batch '{
"Changes": [{
"Action": "UPSERT",
"ResourceRecordSet": {
"Name": "example.com",
"Type": "A",
"AliasTarget": {
"HostedZoneId": "Z35SXDOTRQ7X7K",
"DNSName": "dualstack.web-alb-123456.eu-west-1.elb.amazonaws.com",
"EvaluateTargetHealth": true
}
}
}]
}'
Aliases beat CNAMEs in three ways: they work at the apex, they are free to query where CNAME lookups are billed, and EvaluateTargetHealth lets DNS respond to the target's own health without a separate health check.
The HostedZoneId inside AliasTarget is the target service's zone ID, not yours. Some are fixed values, Z2FDTNDATAQYW2 for CloudFront everywhere, while ALB and NLB ids vary by region. Read it rather than guessing:
aws elbv2 describe-load-balancers --names web-alb \
--query 'LoadBalancers[0].CanonicalHostedZoneId' --output text
Always create AAAA aliases alongside A ones. Skipping it breaks IPv6 clients intermittently, which produces confusing reports from a minority of users.
TTL decides your failover speed
The TTL on a record is how long resolvers cache it, and it sets a floor on how quickly any DNS-based change can take effect.
| TTL | Suits |
|---|---|
| 60 s | Records you may need to fail over |
| 300 s | A sensible general default |
| 3600 s+ | Stable records: MX, TXT, verification |
Lower TTLs mean more queries and a slightly higher bill; that cost is trivial next to an hour-long outage because a record was cached for 3600 seconds. Drop the TTL to 60 before a planned migration, not during it. Lowering it at cutover time does nothing for resolvers already holding the old value.
Alias records have no TTL of their own; they inherit from the target.
Health checks
aws route53 create-health-check --caller-reference "$(date +%s)" \
--health-check-config '{
"Type": "HTTPS",
"FullyQualifiedDomainName": "origin.example.com",
"ResourcePath": "/healthz",
"Port": 443,
"RequestInterval": 30,
"FailureThreshold": 3,
"MeasureLatency": true
}'
Checks run from multiple AWS regions, and a target is considered unhealthy only when more than 18% of checkers agree, which prevents a single region's network problem from triggering a failover.
The practical consequence worth planning around: with RequestInterval: 30 and FailureThreshold: 3, detection takes about 90 seconds, and then resolvers still hold the old answer for the record's TTL. DNS failover is a minutes-scale mechanism, not a seconds-scale one. If you need faster, that belongs at the load balancer or in the client.
Health checks are used by routing policies to remove unhealthy endpoints from answers. Two things to watch: the endpoint must be publicly reachable, since checkers come from the internet, a private ALB needs a CloudWatch-alarm-based check instead, and your security groups must allow Route 53's published checker ranges.
Private hosted zones
A zone that resolves only inside associated VPCs:
aws route53 create-hosted-zone --name internal.example.com \
--caller-reference "$(date +%s)" \
--vpc VPCRegion=eu-west-1,VPCId=vpc-0abc123 \
--hosted-zone-config PrivateZone=true
The VPC needs both enableDnsSupport and enableDnsHostnames set, and a private zone that resolves nowhere is usually one of those being off:
aws ec2 describe-vpc-attribute --vpc-id vpc-0abc123 --attribute enableDnsHostnames
You can have a public and a private zone for the same name, which is split-horizon DNS: inside the VPC, api.example.com resolves to a private address; outside, to a public one. Useful, and confusing to debug. When a record behaves differently depending on where you query from, check for a private zone shadowing the public one.
For resolution between a VPC and on-premises networks, Route 53 Resolver endpoints handle it in both directions.
Debugging DNS
Ask Route 53 what it would answer, bypassing every cache:
aws route53 test-dns-answer --hosted-zone-id Z1ABCDEFGHIJKL \
--record-name api.example.com --record-type A
{
"Nameserver": "ns-123.awsdns-45.com",
"RecordName": "api.example.com",
"RecordType": "A",
"RecordData": ["10.0.1.42"],
"ResponseCode": "NOERROR"
}
That distinguishes "the record is wrong" from "the record is right and something is caching". Then compare with the outside world:
dig +short api.example.com
dig @8.8.8.8 api.example.com
dig NS example.com +short # is delegation correct?
If test-dns-answer returns the right value and dig does not, the problem is delegation, a duplicate zone, or caching, not your record.
Common errors
InvalidChangeBatch: RRSet of type CNAME with DNS name example.com. is not
permitted at apex in zone example.com.
Use an alias record.
InvalidChangeBatch: Tried to create resource record set ... but it already exists
Use UPSERT rather than CREATE.
HostedZoneNotEmpty
Delete every record except the zone's own NS and SOA before deleting the zone.
Records look right, nothing resolves. The registrar is not pointing at this zone's name servers, or there is a second hosted zone for the same domain.
Quick reference
| Task | Command |
|---|---|
| Create a zone | aws route53 create-hosted-zone --name <d> --caller-reference … |
| Get name servers | get-hosted-zone --query 'DelegationSet.NameServers' |
| Change records | aws route53 change-resource-record-sets --change-batch … |
| List records | aws route53 list-resource-record-sets --hosted-zone-id <z> |
| Test authoritative answer | aws route53 test-dns-answer --record-name … --record-type … |
| Create a health check | aws route53 create-health-check |
| Check change status | aws route53 get-change --id <id> |
| ALB alias zone id | describe-load-balancers --query '…CanonicalHostedZoneId' |
Key takeaways
- Nothing resolves until the registrar points at the zone's four name servers.
- Each hosted zone has unique name servers. Recreating a zone breaks delegation.
- Two zones for one domain is a common cause of records that seem to be ignored.
- Use alias records at the apex: free to query, and health-aware.
- The
AliasTargethosted zone ID belongs to the target service, not your zone. - Create AAAA aliases too, or IPv6 clients break intermittently.
- Lower TTLs before a migration, not during it.
- DNS failover takes minutes, between detection and cached TTLs.
test-dns-answerseparates a wrong record from a caching problem.
Check your understanding
Correct records that never resolve
A hosted zone for example.com has correct A records, and test-dns-answer returns the right values. From anywhere else, the domain does not resolve at all. What is wrong?
Failover that took too long
A failover record set with a 30-second health check interval and failure threshold of 3 took roughly fifteen minutes to move traffic during an outage. The record TTL is 900 seconds. Why so slow?
Frequently Asked Questions
Why isn't my Route 53 hosted zone resolving?
Check delegation first. Every hosted zone is assigned four unique name servers, and the domain's registrar must point at exactly those, dig NS example.com shows what the internet is actually being told. The second common cause is duplicate hosted zones for the same domain, where delegation points at one and your records are in the other; list-hosted-zones reveals it. Use test-dns-answer to confirm the zone itself holds the right values, which separates a record problem from a delegation problem.
What is the difference between an alias record and a CNAME?
An alias is a Route 53 extension that points at an AWS resource and is resolved internally, so it can exist at the zone apex where a CNAME cannot, costs nothing to query, and can reflect the target's health via EvaluateTargetHealth. A CNAME is standard DNS, works anywhere except the apex, is billed per query, and simply returns another name for the client to resolve. For AWS targets, ALB, CloudFront, S3 website endpoints, API Gateway, an alias is almost always the right choice.
How do I point my apex domain at a load balancer?
With an alias record, because the DNS specification does not allow a CNAME at the apex. Create an A record for example.com with an AliasTarget whose DNSName is the load balancer's DNS name and whose HostedZoneId is the load balancer's canonical hosted zone ID. Read it from describe-load-balancers --query 'LoadBalancers[0].CanonicalHostedZoneId' rather than guessing, since it differs by region. Add an AAAA alias with the same target for IPv6 clients.
How long does Route 53 failover take?
Longer than most people expect: detection plus caching. Three failed checks at 30-second intervals is about 90 seconds, and the endpoint is only marked unhealthy once more than 18% of Route 53's distributed checkers agree. After that, resolvers continue serving the cached record until its TTL expires. With a 300-second TTL, total failover time is around six or seven minutes. Set TTLs to 60 on records you may need to fail over, and use load balancer health checks where you need sub-minute recovery.
What is a private hosted zone and when would I use one?
A hosted zone that answers only within the VPCs you associate with it, so internal names never appear in public DNS. It is how you give internal services stable names such as db.internal.example.com without exposing your topology. The VPC needs enableDnsSupport and enableDnsHostnames both enabled, which is the usual reason one appears not to work. You can also run a private zone for the same name as a public one, split-horizon DNS, so a hostname resolves to a private address inside and a public one outside.
Why does a record resolve differently inside my VPC than outside?
Almost certainly a private hosted zone for the same domain name associated with that VPC. Route 53 prefers the private zone for queries from inside, so it shadows the public zone entirely for any name it contains, including names it does not define, since the private zone is authoritative for the whole domain. That is split-horizon DNS working as designed, and it is worth checking early whenever DNS behaves differently depending on where you query it from.