Cloud Infrastructure · Cloud Migration
DNS TTL, Resolver Caches, and the Real Duration of a Cutover
How negative caching, recursive resolvers, and connection reuse stretch a five-minute TTL into hours of split traffic, and what actually bounds the redirect window.
A cutover plan that says “lower the TTL to sixty seconds a day before the move” is describing an intention, not a guarantee. The record’s TTL constrains how long a cache may hold the answer. It does not constrain what any particular client does with the answer, and the difference between the two is where cutover windows overrun.
The TTL is a ceiling on cache lifetime, not a promise of expiry
RFC 1034 defines the TTL as the time interval a resolver may cache a resource record. A resolver that receives a record with a TTL of 300 seconds must decrement it on each use and must discard it when it reaches zero. That is the entire obligation. Nothing requires the resolver to re-query at the moment the record expires, and nothing requires a client to re-resolve at all.
Three layers sit between the record and the actual connection, and each adds delay that the TTL does not describe:
The recursive resolver. A resolver serves many clients and answers from cache. When the TTL expires it does not necessarily fetch immediately; it fetches when the next query arrives. Under low query volume, the gap between expiry and refresh can be long, but the answer served after the gap is fresh, so the practical effect is small.
The application. A long-lived process that resolved a hostname at startup holds the address in memory. A connection pool holds established TCP connections to that address. Neither is affected by the record expiring, because neither asks the resolver again. This is the dominant term in most cutovers, and it is invisible from the DNS side.
The client operating system. Some stub resolvers cache independently of the recursive resolver, and some honour the TTL less precisely than the RFC requires. A stub cache that rounds TTLs up to a minimum, or one that caches for a fixed period regardless of the record, extends the effective lifetime in ways the zone owner cannot observe.
The consequence is that the redirect window is at least the maximum TTL in the zone, plus the lifetime of the longest-lived connection that any client holds to the old address, plus however long it takes every client to notice. The first term is under the operator’s control. The second is not, and it is usually the larger of the two.
Connection reuse dominates every TTL decision
A TCP connection is identified by the four-tuple of source address, source port, destination address, and destination port. Once established, it survives changes to the name that produced the destination address, because the name is not part of the connection. A client that resolved the name once and opened a connection will keep using that connection for as long as the server allows.
HTTP keep-alive makes this concrete. A client that sends requests over a persistent connection does not re-resolve the name between requests. A connection held open for hours will keep sending to the old address for hours, regardless of a sixty-second TTL. The only forces that close it are idle timeouts on either end, server-initiated closure, or an error.
This is why the effective cutover duration for a service with persistent connections is governed by the server’s idle timeout and maximum request duration, not by DNS. The practical lever is to shorten those limits before the move so that connections turn over quickly, and to make the old endpoint refuse new connections while allowing existing ones to drain. Draining is the mechanism that converts an unbounded connection lifetime into a bounded one: the old endpoint stops accepting, signals closure on idle connections, and waits for in-flight requests to finish.
Negative caching and the record that does not exist
A cutover that creates a new name in a new zone, rather than changing an existing record’s value, encounters negative caching. RFC 2308 defines how a resolver caches the fact that a name does not exist, using the SOA record’s minimum TTL field to bound the lifetime of that negative answer.
The failure this produces is specific. A client queries the new name before it exists, receives NXDOMAIN, and caches that answer for the negative TTL. The name is then created, but the client keeps receiving the cached negative answer until the negative TTL expires. If the zone’s SOA minimum is a day, the new name is unreachable from that client for a day even though the record exists.
The mitigation is to create the record before any client can query it, and to ensure that the SOA minimum is short for the duration of the migration. A record that exists with a placeholder value is not cached negatively; a record that does not exist is. The distinction is worth an explicit step in the plan, because it is one of the few failure modes that is entirely preventable and entirely invisible until it bites.
Serving stale data extends the window past the TTL
RFC 8767 permits a resolver to serve a stale cached answer when it cannot reach the authoritative server, bounded by a separate stale timer. The mechanism exists so that a resolver outage does not become a resolution outage for clients. For a migration, it means that a record which has already expired at the resolver can still be served, with the old value, if the authoritative servers are unreachable.
The scenario is not exotic. A cutover that changes authoritative nameservers for the zone, and gets the delegation wrong, makes the old servers unreachable from some resolvers while others still have the old records. Those resolvers serve stale answers, and the stale answers point at the old address. The cutover appears to work for the clients behind well-behaved resolvers and fails for the rest.
The defence is to change one thing at a time. Change the record value while the delegation is stable, verify, and only then change the delegation. Changing both in one step makes the two failure modes indistinguishable.
What actually bounds the redirect window
Given the mechanisms above, the window has four additive components, and only the first is directly configurable.
The maximum TTL across every record the service depends on. Not just the primary name: aliases, mail exchanger records, service records, and any CNAME chain each carry their own TTL, and the chain’s effective lifetime is the sum along the path, because each cache along the chain holds its own copy.
The maximum connection lifetime. Bounded by the server’s idle and request timeouts, by client behaviour, and by any middlebox that holds connections open. A load balancer with a long idle timeout keeps client connections alive to the old backend pool well past the DNS change.
The client re-resolution interval. A client that re-resolves on each new connection will follow the change within one connection lifetime. A client that resolves once at process start will follow it only on restart. Nothing in the protocol distinguishes the two cases from the outside.
The observation delay. The time between the change taking effect and the operator noticing that a client is still on the old path. This is a monitoring property, and it is the one that determines how long a partially redirected state persists. Measuring traffic at the old endpoint, rather than assuming it has stopped, is what converts this term from unbounded to bounded.
The last component is why a cutover should be watched at both ends rather than declared complete at the moment of the change. A plan that treats the DNS update as the end of the migration will leave the old endpoint running for an unknown period, and will leave the two datasets diverging for that period. Keeping the old endpoint writable for a defined grace period, then freezing it, then reconciling, is the shape that makes the window explicit rather than implicit. That freeze is also the point at which rollback stops being a matter of pointing the name back and becomes a reconciliation problem.
Where the indirection should sit
The alternative to a long window is to make the name change only once, and to put the indirection somewhere that changes fast. A name that resolves to a load balancer, with the balancer’s backend pool switched, changes in one place and propagates in seconds. The clients still hold connections, but those connections terminate at the balancer, which can move them to the new pool without the client re-resolving anything.
This is the standard reason migrations route through an indirection layer rather than repointing names. It converts a distributed cache-expiry problem into a single control-plane update. The cost is an extra hop and a component that must itself be migrated or kept, and the benefit is that the DNS TTL becomes almost irrelevant to the cutover duration.
Where the indirection cannot be inserted, because the client connects directly to a hostname it resolves itself, the TTL and the connection lifetime both matter and neither can be shortened from the server side alone. In that case the only reliable method is to keep both endpoints live and correct for the duration of the window, which means the destination must be kept in sync, which means the change stream has to stay running until the last client has moved.