DNS Cutover for Cloud Migration: TTL, Weighting, Rollback

A safe DNS cutover for a cloud migration is not a single flip. Lower the record's TTL to 60 seconds at least a full day before the move so resolvers stop caching the old answer, shift a small slice of traffic to the cloud endpoint with a Route 53 weighted record, watch it, then raise the weight until the on-premises endpoint is at zero. Keep the old target reachable so you can drop the weight back if something breaks. The hard part is not the DNS change itself, it is the caching you do not control and the connections that never re-resolve.
This article covers the AWS Route 53 case. The same mechanics apply to Azure DNS and Google Cloud DNS with different names for weighted routing, but the caching and TTL problems are identical everywhere.
Check these before you touch a record
Three things decide whether your cutover is boring or a bad afternoon.
The current TTL and where the record actually lives. Find the authoritative TTL on the record you are moving and confirm which hosted zone is authoritative for the name. A common failure is editing a record in a zone that is not the one your registrar delegates to, so the change does nothing. <cite index="9497587-2">When you change the name servers for the domain to the name servers from your Route 53 hosted zone, it can take up to two days for the change to take effect, because DNS resolvers across the internet typically request the name servers only once every two days and cache the answer.</cite> If the migration also moves the zone itself, do that delegation change weeks earlier as a separate step. Do not combine a delegation change with a traffic cutover.
Whether clients hold long-lived connections. DNS only steers new sessions. <cite index="d9db61ff-0">Clients typically only perform a DNS request when initiating a session, so if you have a long-lived TCP session that is active or has an extended timeout, that client may not move to a different endpoint even after you change the record.</cite> Database connection pools, gRPC channels, message-broker consumers and keep-alive HTTP clients can sit on the old IP for hours. For those, plan an explicit drain or a forced reconnect, and do not assume the DNS change moved them.
What the record points at. If it is an A or AAAA record to an on-prem load balancer VIP, you can weight it against the cloud endpoint's IP. If the cloud target is an ALB or CloudFront, you will use an alias record, and weighted alias records are their own record type with the same weighting rules. Confirm the shape now so you are not inventing record types during the change window.
Lower the TTL first, then wait it out
TTL is the single most important lever and the one people skip. <cite index="cc2d0e5f-3,cc2d0e5f-4">Each record has a TTL value that specifies how long, in seconds, DNS resolvers cache the information in the record, and until that time passes, resolvers continue to return the old value in response to queries.</cite>
If your record sits at a 3600-second TTL, a resolver that cached it one second before your change keeps serving the old IP for an hour. Lower it well ahead of time. For a record you are about to move, 60 seconds is reasonable during the window.
The order matters. Set the low TTL, then wait longer than the old TTL so every cached copy of the previous value has expired. Only then start shifting traffic. If you lower the TTL and cut over in the same ten minutes, resolvers are still holding the old high-TTL answer and your careful weighting does nothing for them.
# Lower the TTL a day ahead. This does not move traffic,
# it just makes the later change take effect quickly.
aws route53 change-resource-record-sets \
--hosted-zone-id Z123EXAMPLE \
--change-batch '{
"Changes": [{
"Action": "UPSERT",
"ResourceRecordSet": {
"Name": "app.example.com",
"Type": "A",
"TTL": 60,
"ResourceRecords": [{"Value": "203.0.113.10"}]
}
}]
}'
One honest caveat: some recursive resolvers and client stacks clamp or ignore TTLs, and a low TTL means more queries and a little more latency. <cite index="7b0daf75-0">Caching DNS resolvers are outside the control of Route 53 and will cache your resource record sets according to their TTL.</cite> A short TTL shrinks the exposure window, it does not eliminate stragglers. This is why you keep the old endpoint alive after the flip.
Shift traffic with weighted records, not one flip
Instead of replacing the on-prem value with the cloud value in one edit, create two records with the same name and type, one per endpoint, and control the split by weight. <cite index="e94b6a04-0">To set up weighted routing you create records with the same name and type for each resource and give each a relative weight, and Route 53 sends traffic to a resource based on its weight as a share of the total weight for all records in the group.</cite>
The weight math is a ratio, not a percentage. <cite index="e94b6a04-0">If you set weights of 1 and 255, the resource with weight 1 gets 1/256th of the traffic and the other gets 255/256ths, and you can slowly change the balance by changing the weights.</cite> Start the cloud record at a small weight, confirm error rates and latency on the new path, then move the weight up in steps. To retire the old endpoint, <cite index="e94b6a04-0">change its weight to 0 to stop sending traffic to it.</cite>
resource "aws_route53_record" "app_onprem" {
zone_id = var.zone_id
name = "app.example.com"
type = "A"
ttl = 60
set_identifier = "onprem"
weighted_routing_policy { weight = 90 }
records = ["203.0.113.10"]
}
resource "aws_route53_record" "app_cloud" {
zone_id = var.zone_id
name = "app.example.com"
type = "A"
ttl = 60
set_identifier = "cloud"
weighted_routing_policy { weight = 10 }
records = ["198.51.100.20"]
}
Roll it forward by editing weights: 90/10, then 50/50, then 10/90, then 0/100. Rollback is the same edit in reverse, and because the TTL is 60 seconds, resolvers pick up the change within about a minute. Keep both endpoints healthy the whole time. Setting a weight to 0 stops new traffic, it does not tear anything down, which is exactly what you want during a migration.
Health checks and a rollback that does not need a human
Weighting is a manual dial. For automatic rollback, attach a Route 53 health check to each weighted record so an endpoint that is failing gets pulled from responses without anyone editing a record at 2am.
A health check has a few knobs worth setting deliberately. <cite index="0e28d3f0-0">You choose how often Route 53 sends a request (the request interval) and how many consecutive failures are needed before the endpoint is considered unhealthy (the failure threshold), and you can have Route 53 set a CloudWatch alarm that notifies you through SNS.</cite> <cite index="c8fce18a-0">You can specify an interval of every 10 seconds or every 30 seconds.</cite> <cite index="7c81ba81-1">The failover threshold can be set from 1 to 10 consecutive observations, with a default of 3.</cite> A 10-second interval with a threshold of 3 detects a dead endpoint in roughly 30 seconds of checking, before the DNS TTL even expires.
Know what "healthy" means to Route 53 so your check does not lie. <cite index="7c81ba81-0,7c81ba81-2">For HTTP and HTTPS health checks, Route 53 must establish a TCP connection within four seconds and the endpoint must respond with a 2xx or 3xx status within two seconds after connecting, and HTTPS checks do not validate the certificate.</cite> Point the check at a real dependency-aware health endpoint, not the load balancer's default page, or you will pass health checks while the app is down.
If you prefer explicit primary and standby behaviour over weighting, use a failover routing policy instead. <cite index="fa1ff0b9-1">For failover to function correctly you must create one primary and one secondary failover record.</cite> <cite index="ef27e12a-0">Route 53 returns the primary resource while it is healthy, automatically responds with the secondary when the primary is unhealthy and the secondary is healthy, and returns the primary if both are unhealthy.</cite> Failover is cleaner for a two-state cutover with a defined rollback target. Weighting is better when you want a gradual, observable ramp.
Planning the connection draining, the health endpoints and the on-call runbook around this is where a migration goes from risky to routine. That is the core of our cloud migration work, and the health-check and alerting design overlaps directly with how we set up reliability and SRE practices.
The trade-offs, and what each costs later
| Approach | Rollback speed | Best for | What it costs later |
|---|---|---|---|
| Hard flip (edit the value) | Bounded by old TTL, often minutes to hours | Trivial internal records nobody caches long | Slow, unobservable rollback; stragglers on the old cache |
| Weighted records + low TTL | ~1 min at 60s TTL, manual dial | Gradual, watched production cutovers | More DNS queries and slight latency from the low TTL |
| Failover records + health checks | Automatic, seconds of detection plus TTL | Two-state cutover with a clear standby | Only two states; a bad health endpoint hides real failure |
The recurring cost across all three is the low TTL you leave in place and the resolvers that ignore it. Raise the TTL back to a normal production value only after the cutover is confirmed and you have decommissioned the old target. Do not raise it while you might still need to roll back. And remember the whole scheme steers new sessions only: long-lived connections need their own drain, or they will quietly keep talking to the datacentre you are trying to leave.
Frequently asked questions
How long should I lower the DNS TTL before a migration cutover?
Set the record to 60 seconds, then wait longer than the record's previous TTL before you move any traffic, so every cached copy of the old value has expired. If the old TTL was 3600 seconds, wait at least an hour, and ideally schedule the TTL reduction a day ahead.
Why is traffic still hitting my old server after I changed the DNS record?
Two common causes. Resolvers are still serving the cached old value until the TTL expires, so lower the TTL ahead of time and wait it out. <cite index="cc2d0e5f-4">Until the TTL passes, DNS resolvers continue to return the old value.</cite> The other cause is long-lived connections: <cite index="d9db61ff-0">clients on active or long-timeout TCP sessions may not move even after the record changes,</cite> so those need an explicit reconnect or drain.
Should I use weighted routing or failover routing for a cutover?
Use weighted records when you want a gradual, observable ramp you control by editing weights, moving from 90/10 to 0/100. Use failover routing when you want an explicit primary and standby with automatic switchover; <cite index="fa1ff0b9-1">it requires one primary and one secondary record.</cite> Many migrations use weighted routing for the ramp and keep a health-checked path so a failing endpoint is pulled automatically.
How fast will Route 53 health checks detect a failed endpoint?
<cite index="c8fce18a-0">You can set the request interval to 10 or 30 seconds,</cite> and <cite index="7c81ba81-1">the failure threshold to between 1 and 10 consecutive observations, defaulting to 3.</cite> A 10-second interval with a threshold of 3 detects a dead endpoint in roughly 30 seconds. Resolvers then stop getting that record on their next query, bounded by your TTL.
Does a low TTL guarantee everyone sees my DNS change quickly?
No. <cite index="7b0daf75-0">Caching resolvers are outside Route 53's control and cache records according to their TTL,</cite> and some resolvers and client libraries clamp or ignore short TTLs. A low TTL shrinks the window of stragglers, it does not remove them, which is exactly why you keep the old endpoint reachable until well after the cutover.


