DNS is the one system where a bad change is felt everywhere at once, and Route 53's asynchronous, batch-oriented API rewards a few disciplined habits. A wrong record, a TTL that traps clients on a dead endpoint, a delete that did not match - each is a small mistake with an outsized blast radius.
Walk this list once now, then use it as a review checklist before shipping any code that creates zones, writes records, or shapes traffic.
Prefer UPSERT for writes.UPSERT creates-or-overwrites, so re-running a script is safe; CREATE fails on an existing record and DELETE needs an exact match.
Use a unique CallerReference. Generate a fresh idempotency token per CreateHostedZone / CreateHealthCheck so retries never make duplicates.
Batch related changes atomically. Group a delete-and-replace or a whole record set into one ChangeBatch; the batch applies all-or-nothing.
Read before you delete.DELETE requires every field to match the live record; list it with ListResourceRecordSets and pass it straight back.
Use an idempotent UPSERT so the write is safe to repeat.
Wait for INSYNC before depending on a record. Use the waiter (resource_record_sets_changed / waitUntilResourceRecordSetsChanged) instead of a fixed sleep.
Capture the ChangeInfo id. Keep the change id from the response so you can GetChange or wait on exactly that change.
Do not assume instant resolution. Propagation is usually under a minute, but a script that resolves immediately after writing can race.
Paginate list calls.ListResourceRecordSets and ListHostedZones page; use a paginator so large zones are read fully.
Wait for the change to reach INSYNC rather than guessing.
Resolve canonical zone ids at runtime. Pull an ALB's CanonicalHostedZoneId from DescribeLoadBalancers; only CloudFront's Z2FDTNDATAQYW2 is safe to inline.
Use an alias, not a CNAME, at the apex. A CNAME is invalid at the zone apex; an alias A/AAAA record points the apex at AWS resources.
Prefer aliases for AWS targets. Alias queries to AWS resources are not billed and need no TTL tuning, unlike a CNAME.
Set EvaluateTargetHealth deliberately. Turning it on ties the alias to the target's health, adding free failover for ELB and other targets.
Scope IAM to specific zones. Grant route53:ChangeResourceRecordSets on the exact hosted-zone ARNs rather than *, so a bug cannot rewrite every zone.
Set enableDnsSupport and enableDnsHostnames for private zones. Both VPC attributes must be on or instances cannot resolve the private zone.
Delete records before the zone.DeleteHostedZone fails while non-NS/SOA records remain; clear them first.
Sweep for stale records and health checks. Remove records that point at deleted resources and orphaned health checks that still bill.
Rely on built-in retries. Both SDKs retry throttling (Throttling/PriorRequestNotComplete) with backoff; tune retry settings rather than reimplementing them.
UPSERT creates-or-overwrites, so a script is safe to re-run. CREATE fails if the record already exists, and DELETE requires an exact field match, both of which break idempotent automation.
How should I manage TTLs around a change?
Lower the TTL (to ~60s) a TTL-length before a planned cutover so clients pick up the new answer fast, then raise it (to 300-3600s) once the change is stable to cut query cost.
Does INSYNC mean my record resolves everywhere?
No. INSYNC means Route 53's own servers agree. Resolver and client caches still honor the previous TTL until it expires, so plan cutovers around the TTL you set.
How do I make a DNS write safe to retry?
Use UPSERT for record changes and a fresh unique CallerReference for CreateHostedZone and CreateHealthCheck, so a retry overwrites or returns the existing resource instead of duplicating.
Why should routing records have health checks?
Without a HealthCheckId, a weighted, latency, or failover record keeps returning its endpoint even when it is down. A failover primary in particular never fails over without one.
Where do I get the AliasTarget hosted zone id?
From the target: an ALB's CanonicalHostedZoneId via DescribeLoadBalancers, an S3 website endpoint's per-region id, or the CloudFront constant Z2FDTNDATAQYW2. Resolve the variable ones at runtime.
How do I wait for a change safely?
Capture ChangeInfo.Id from the response and use the waiter - resource_record_sets_changed in boto3 or waitUntilResourceRecordSetsChanged in SDK v3 - instead of a fixed sleep.
How should I scope Route 53 IAM permissions?
Grant ChangeResourceRecordSets on specific hosted-zone ARNs rather than *, so a mistake or compromised credential cannot rewrite every zone in the account.
Why can't I delete my hosted zone?
DeleteHostedZone fails while the zone still holds records other than the managed NS and SOA. Delete those records first, then delete the zone.
Do I need my own retry logic for Route 53?
Usually no. Both SDKs retry throttling and PriorRequestNotComplete with backoff. Tune the retry mode and max attempts instead of reimplementing retries by hand.
Why won't my private zone resolve?
Most often because the VPC's enableDnsSupport and enableDnsHostnames attributes are not both enabled. Set them with ModifyVpcAttribute before expecting private DNS to work.