dnsprobe v1

How DNS propagation actually works (and why your change is not live yet)

· ~3 min read · dnsprobe.net/blog

The first thing to internalise is that DNS does not propagate. The word is a lie of convenience that has stuck since the late 1990s, and it papers over the actual mechanism. The authoritative nameservers for your zone are updated the instant you save the record at your DNS provider. There is no replication queue, no eventual-consistency window at that layer. The change is live.

What you are actually waiting for is recursive resolver caches. Cloudflare's 1.1.1.1, Google's 8.8.8.8, your ISP's resolver, your office DNS forwarder, your machine's stub resolver — each of these holds the previous answer until the TTL it was served with elapses. The TTL is set by you (or your provider's defaults), and it is the only knob you have over how fast the recursive layer drops the stale record.

The transaction, end to end

A client asks for github.com. The path of that query, assuming a cold cache the whole way down, looks like this:

stub resolver (libc / systemd-resolved)
  └─→ recursive resolver (1.1.1.1, ISP, etc.)
        ├─→ root server         "where are .com NS?"
        ├─→ .com TLD server     "where are github.com NS?"
        └─→ github.com NS       "what is the A for github.com?"
                                  → 140.82.112.4 (TTL 60)

That answer travels back up the chain. The recursive resolver writes it to its cache with the TTL the authoritative server provided. Every subsequent query for github.com hitting that resolver, for the next 60 seconds in this example, returns the cached answer with the remaining TTL decremented. No re-walking, no DNSSEC re-validation, nothing crosses the wire to GitHub's nameservers.

This is why "propagation" feels uneven. Two different visitors to the same website hit two different recursive resolvers, each with a different cache-fill timestamp, each counting down its own copy of the TTL.

The TTL is the entire game

If your zone records carry a TTL of 14400 (4 hours), then in the worst case a freshly updated record will not be visible to every recursive resolver until 4 hours after the last query that filled its cache with the old value. Not 4 hours from when you made the change. The clock at each resolver started when it last filled.

This is why mature ops teams lower the TTL 24-48 hours before a planned change:

# before the change window: announce the lower TTL
example.com.    A    203.0.113.10    TTL 60

# wait at least one old-TTL cycle so every cache picks up the 60s value
# then make the actual change
example.com.    A    203.0.113.20    TTL 60

# afterwards, raise it back if you want
example.com.    A    203.0.113.20    TTL 3600

The window has to be at least one full prior-TTL because resolvers that cached during the old window have no idea you've shortened the TTL — they will keep serving the old IP at the old TTL until that copy ages out.

Where dnscheck fits

The whole point of running a tool like dnscheck after a change is to inspect which recursive resolvers have caught up. A response of "all 12 agree" is a strong signal that the major public resolvers (and by proxy, most consumer ISPs that forward to them) are now serving the new value. Try it on a domain whose name you know well — say github.com — to see the per-resolver agreement matrix.

The caveats nobody mentions

Three things bite people who treat "all resolvers green" as ground truth:

  1. Negative caching. If a resolver tried to look up your record before you created it and got NXDOMAIN, that negative answer is also cached, governed by the SOA minimum TTL field (RFC 2308). You can ship the record and still get NXDOMAIN at a resolver for the minimum-TTL window.
  2. CDN and anycast. Your "DNS for the cert" check is irrelevant when a CDN like Cloudflare or Fastly returns a different A per region. The propagation question shifts from "is the answer the same" to "is the answer the same within this region".
  3. Stub-resolver caches. macOS's mDNSResponder, systemd-resolved, the browser's own DNS cache, the in-process JVM resolver cache — each adds its own TTL. sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder on macOS, sudo resolvectl flush-caches on Linux.

The mental model that scales

Stop asking "has it propagated yet?" Ask instead: "which resolver am I about to query, and when did it last fill its cache for this name?" The first question has no answer. The second one is observable and gives you a strategy.

See also: RFC 1034 (Domain Names — Concepts), RFC 2308 (Negative Caching), and Cloudflare's walkthrough of 1.1.1.1's cache.