AWS Route 53 Records
Every record type you will actually use, A, CNAME, MX, TXT, SRV and CAA, plus UPSERT semantics, SPF and DMARC, and the errors Route 53 returns.
Route 53 Records
A hosted zone is a collection of record sets. Route 53 calls them "record sets" rather than records because one entry can hold several values. An A record with three addresses is one record set, not three records, and that distinction matters when you update them.
The change batch API
Every write goes through change-resource-record-sets with a batch of changes, applied atomically:
aws route53 change-resource-record-sets --hosted-zone-id Z1ABCDEFGHIJKL --change-batch '{
"Comment": "add api endpoint",
"Changes": [{
"Action": "UPSERT",
"ResourceRecordSet": {
"Name": "api.example.com",
"Type": "A",
"TTL": 60,
"ResourceRecords": [{"Value": "203.0.113.10"}]
}
}]
}'
{ "ChangeInfo": { "Id": "/change/C2ABCDEFGHIJKL", "Status": "PENDING" } }
Use UPSERT, not CREATE. CREATE fails if the record exists, which makes scripts non-idempotent; UPSERT creates or replaces. DELETE requires the record's exact current contents: value, TTL and all, which is why deleting is fiddlier than it looks and another reason to prefer UPSERT.
Because a batch is atomic, one invalid change rejects the whole thing. That is useful: a coordinated cutover of several records happens together or not at all.
aws route53 wait resource-record-sets-changed --id /change/C2ABCDEFGHIJKL
Multiple values go in one record set, and DNS returns them in rotating order. Crude round-robin with no health awareness:
"ResourceRecords": [
{"Value": "203.0.113.10"},
{"Value": "203.0.113.11"}
]
A and AAAA
# A. IPv4
{"Name": "api.example.com", "Type": "A", "TTL": 60,
"ResourceRecords": [{"Value": "203.0.113.10"}]}
# AAAA. IPv6
{"Name": "api.example.com", "Type": "AAAA", "TTL": 60,
"ResourceRecords": [{"Value": "2001:db8::1"}]}
If you publish an AAAA record, the service behind it must work over IPv6. Clients with IPv6 connectivity prefer it, so a broken AAAA record produces slow or failing requests for a subset of users while everyone else is fine. One of the more frustrating reports to receive.
CNAME, and where it cannot go
{"Name": "www.example.com", "Type": "CNAME", "TTL": 300,
"ResourceRecords": [{"Value": "example.com"}]}
Two rules, both from the DNS specification rather than AWS:
No CNAME at the zone apex. example.com cannot be a CNAME, because the apex must hold SOA and NS records and a CNAME cannot coexist with other records at the same name. Route 53 rejects it explicitly:
InvalidChangeBatch: RRSet of type CNAME with DNS name example.com. is not
permitted at apex in zone example.com.
Use an alias record instead.
A CNAME cannot share a name with any other record type. So www cannot be both a CNAME and hold a TXT record, which matters when a service asks you to add a verification TXT record on a hostname that is already a CNAME.
MX, and the mail records that go with it
{"Name": "example.com", "Type": "MX", "TTL": 3600,
"ResourceRecords": [
{"Value": "1 aspmx.l.google.com"},
{"Value": "5 alt1.aspmx.l.google.com"},
{"Value": "10 alt2.aspmx.l.google.com"}
]}
The number is priority, lowest first. A frequent misreading, since "highest priority" means the smallest number.
Mail needs three more records, and getting them wrong is why your application's email lands in spam:
# SPF, which servers may send as you. A TXT record; the SPF type is deprecated.
{"Name": "example.com", "Type": "TXT", "TTL": 3600,
"ResourceRecords": [{"Value": "\"v=spf1 include:_spf.google.com include:amazonses.com ~all\""}]}
# DKIM. Signing keys, usually provided by the mail service
{"Name": "selector1._domainkey.example.com", "Type": "CNAME", "TTL": 3600,
"ResourceRecords": [{"Value": "selector1.domainkey.u123.wl.sendgrid.net"}]}
# DMARC. What receivers should do when SPF and DKIM fail
{"Name": "_dmarc.example.com", "Type": "TXT", "TTL": 3600,
"ResourceRecords": [{"Value": "\"v=DMARC1; p=quarantine; rua=mailto:dmarc@example.com\""}]}
Only one SPF record per domain. Two TXT records both starting v=spf1 is a permanent error that silently breaks authentication. Combine them into one with multiple include: terms. Note the maximum of ten DNS lookups in an SPF record, which several include: entries can exceed.
Start DMARC at p=none with rua= reporting to see what is being sent as you, then tighten to quarantine and reject once legitimate senders are covered.
TXT records and quoting
TXT values must be quoted, and those quotes are part of the value, so in a shell-embedded JSON payload they need escaping, which is where most TXT mistakes come from:
{"Value": "\"v=spf1 include:amazonses.com ~all\""}
A single string is limited to 255 characters. Longer values are split into multiple quoted strings which receivers concatenate. DKIM keys frequently need this:
{"Value": "\"v=DKIM1; k=rsa; p=MIGfMA0GCS...first255\" \"...remainder\""}
Get the quoting wrong and the record exists, looks plausible in the console, and fails validation with no useful error from whichever service is checking it.
CAA
Restricts which certificate authorities may issue for your domain:
{"Name": "example.com", "Type": "CAA", "TTL": 3600,
"ResourceRecords": [
{"Value": "0 issue \"amazon.com\""},
{"Value": "0 issue \"amazontrust.com\""},
{"Value": "0 iodef \"mailto:security@example.com\""}
]}
Worth setting, with one caution: a CAA record that omits your CA blocks issuance and renewal. If ACM certificates suddenly fail to validate, check whether someone added a CAA record without including amazon.com.
SRV
{"Name": "_sip._tcp.example.com", "Type": "SRV", "TTL": 300,
"ResourceRecords": [{"Value": "10 60 5060 sipserver.example.com"}]}
The four fields are priority, weight, port and target. Used by SIP, XMPP, Minecraft and Kubernetes service discovery.
Listing and auditing
aws route53 list-resource-record-sets --hosted-zone-id Z1ABCDEFGHIJKL \
--query 'ResourceRecordSets[].[Name,Type,TTL,ResourceRecords[0].Value]' \
--output table
# Find records with long TTLs before a migration
aws route53 list-resource-record-sets --hosted-zone-id Z1ABCDEFGHIJKL \
--query 'ResourceRecordSets[?TTL>`600`].[Name,Type,TTL]' --output table
That second query is worth running before any planned cutover. It tells you which records will keep serving stale answers, and for how long.
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 [name='api.example.com.',
type='A'] but it already exists
CREATE on an existing record. Use UPSERT.
InvalidChangeBatch: Tried to delete resource record set ... but it was not found
DELETE needs the exact current value and TTL. Read it with list-resource-record-sets first.
InvalidChangeBatch: RRSet with DNS name www.example.com. is not permitted
because a CNAME record exists with the same name
A CNAME cannot coexist with another type at the same name.
Quick reference
| Record | Purpose | Note |
|---|---|---|
A | IPv4 address | |
AAAA | IPv6 address | Only if the service really supports IPv6 |
CNAME | Alias to another name | Not at the apex; not alongside other types |
Alias | Route 53 alias to an AWS resource | Free, works at the apex |
MX | Mail servers | Lower number wins |
TXT | Arbitrary text | Quoted; 255-char strings |
NS | Delegation | Also delegates subdomains |
CAA | Permitted certificate authorities | Omitting your CA blocks issuance |
SRV | Service location | priority weight port target |
Key takeaways
- Use
UPSERT.CREATEis not idempotent andDELETEneeds exact current contents. - Change batches are atomic: one bad change rejects the lot, which makes coordinated cutovers safe.
- No CNAME at the apex, and no CNAME alongside another record type.
- MX priority is lowest-wins.
- Exactly one SPF record, combined with multiple
include:terms. - TXT quoting is part of the value, and single strings cap at 255 characters.
- A CAA record without your CA blocks certificate issuance.
- Audit TTLs before a migration: that is what decides how long stale answers persist.
Check your understanding
Two SPF records
A domain has one TXT record with "v=spf1 include:_spf.google.com ~all" and a second with "v=spf1 include:amazonses.com ~all". Mail from SES is failing authentication. Why?
A verification record that cannot be added
A SaaS provider asks for a TXT verification record at www.example.com. That name is already a CNAME to the application. Route 53 refuses to create the TXT record. Why?
Frequently Asked Questions
What is the difference between CREATE, UPSERT and DELETE in a change batch?
CREATE adds a record and fails if one already exists with that name and type, which makes scripts non-idempotent. UPSERT creates the record or replaces it entirely, so re-running a deployment is safe. It is the action to use by default. DELETE removes a record but requires you to supply its exact current contents, including every value and the TTL, so it usually means reading the record first. Batches are applied atomically, so an invalid change rejects the whole submission.
Why can't I create a CNAME at my apex domain?
Because the DNS specification forbids it. The zone apex must carry SOA and NS records, and a CNAME cannot coexist with any other record type at the same name, so an apex CNAME is impossible in any DNS system, not just Route 53. The Route 53 answer is an alias record, which behaves like a CNAME pointing at an AWS resource but is resolved internally and returns an address, so it is legal at the apex. It is also free to query and can reflect the target's health.
How do I set up SPF, DKIM and DMARC in Route 53?
SPF is a single TXT record at the apex listing permitted senders, and it must be exactly one record, with multiple include: terms rather than multiple records, since two v=spf1 records is a permanent error. DKIM is normally a set of CNAMEs your mail provider gives you, pointing at keys they host. DMARC is a TXT record at _dmarc.example.com; start at p=none with a rua= reporting address to see what is being sent as your domain, then tighten to quarantine and reject.
Why is my TXT record not being recognised?
Usually quoting. The value must include literal double quotes, which means escaping them inside a JSON change batch, "\"v=spf1 include:amazonses.com ~all\"". Get it wrong and the record exists, looks plausible, and fails whatever validation is checking it. The other cause is length: a single TXT string is capped at 255 characters, so a longer value such as a DKIM public key must be split into multiple quoted strings within the same record, which receivers concatenate.
What does the number in an MX record mean?
Priority, and lower wins, which is the opposite of how "priority" reads to most people. 1 aspmx.l.google.com is tried before 10 alt2.aspmx.l.google.com, and the higher numbers act as fallbacks. Equal numbers share traffic. All of a domain's MX values belong in a single record set with one value per line, not as separate record sets, since they collectively describe where mail for that domain should go.
Should I add a CAA record?
Yes, with care. A CAA record restricts which certificate authorities may issue certificates for your domain, which limits the damage from a mis-issued certificate. The caution is that omitting a CA you use blocks issuance and renewal, so if you use ACM, include amazon.com and amazontrust.com, and add any other authority in use before publishing the record. Sudden unexplained ACM validation failures are worth checking against CAA before anything else.