Changing a domain’s nameservers is safe when two things hold: during the overlap, the old provider and Cloudflare return the same answers for every name, and the old provider keeps serving until no resolver can still be using it. Most outages during a move come from breaking one of those, usually a record the new zone never received, a DS record left at the registrar, or an old zone deleted too early.

This runbook puts the steps in order. The captures come from and.guide, whose DNS is already on Cloudflare; we did not migrate anything for this article, we only observed our current delegation and zone so you can compare your own output.

What actually changes at the registry

Your registrar sends the new nameserver names to the registry that runs your TLD, and the registry publishes them in the parent zone as the delegation. Resolvers learn your nameservers from that delegation, so you can check it yourself by asking a TLD server directly.

Live capture, 3 October 2026 (03:43 UTC). First the servers for .guide, then one of them asked about and.guide with recursion off:

$ dig +nocmd guide NS +short
v0n0.nic.guide.
v0n2.nic.guide.
v2n0.nic.guide.
v0n3.nic.guide.
v0n1.nic.guide.
v2n1.nic.guide.
$ dig +nocmd +norec and.guide NS @v0n0.nic.guide
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 38712
;; flags: qr; QUERY: 1, ANSWER: 0, AUTHORITY: 2, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;and.guide.			IN	NS

;; AUTHORITY SECTION:
and.guide.		3600	IN	NS	roan.ns.cloudflare.com.
and.guide.		3600	IN	NS	frida.ns.cloudflare.com.

;; Query time: 39 msec
;; SERVER: 65.22.28.12#53(65.22.28.12)
;; WHEN: Sat Oct 03 12:43:48 KST 2026
;; MSG SIZE  rcvd: 94

This is a referral, which RFC 9499 describes as a server signaling that it is not (completely) authoritative and pointing the resolver elsewhere. There is no aa flag, ANSWER: 0, and the NS records sit in the authority section. The 3600 on those records is the registry’s choice, not ours. The moment your registrar’s change reaches the registry, this answer changes; until then nothing you do at Cloudflare matters to the rest of the internet.

The delegation TTL differs by TLD.

Live capture, 3 October 2026 (03:44 UTC). The first NS record of the delegation for a few well-known domains, each asked of one of its TLD’s servers:

$ dig +nocmd +norec and.guide NS @v0n0.nic.guide +noall +authority | head -1
and.guide.		3600	IN	NS	roan.ns.cloudflare.com.
$ dig +nocmd +norec cloudflare.com NS @a.gtld-servers.net +noall +authority | head -1
cloudflare.com.		172800	IN	NS	ns3.cloudflare.com.
$ dig +nocmd +norec cloudflare.net NS @b.gtld-servers.net +noall +authority | head -1
cloudflare.net.		172800	IN	NS	ns1.cloudflare.net.
$ dig +nocmd +norec isc.org NS @b0.org.afilias-nst.org +noall +authority | head -1
isc.org.		3600	IN	NS	ns2.isc.org.
$ dig +nocmd +norec desec.io NS @b0.nic.io +noall +authority | head -1
desec.io.		3600	IN	NS	ns2.desec.org.
$ dig +nocmd +norec web.dev NS @ns-tld3.charlestonroadregistry.com +noall +authority | head -1
web.dev.		10800	IN	NS	ns3.zdns.google.
TLD Delegation NS TTL observed In hours
.com, .net 172800 48
.dev 10800 3
.guide, .org, .io 3600 1

A resolver that cached a .com referral just before your change may keep sending queries to the old nameservers for up to two days. You can’t lower this number; the registry sets it.

The second NS TTL: your zone’s own

The parent’s copy isn’t the only NS record set. RFC 2181 says the NS records at a zone cut are “the property of the child zone,” and it ranks data from an authoritative answer above data from the authority section of a referral. So a resolver that asks your nameservers for your NS records can replace the parent’s copy with your zone’s, and keep that for your zone’s TTL instead.

Live capture, 3 October 2026 (03:44 UTC). and.guide’s NS records from one of our Cloudflare nameservers, then from 1.1.1.1, 8.8.8.8, and our ISP’s default resolver:

$ dig +nocmd @roan.ns.cloudflare.com and.guide NS +norec +noall +comments +answer | grep -E "flags|NS"
;; flags: qr aa; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
; EDNS: version: 0, flags:; udp: 1232
;; ANSWER SECTION:
and.guide.		86400	IN	NS	frida.ns.cloudflare.com.
and.guide.		86400	IN	NS	roan.ns.cloudflare.com.
$ dig +nocmd @1.1.1.1 and.guide NS +noall +answer
and.guide.		86400	IN	NS	frida.ns.cloudflare.com.
and.guide.		86400	IN	NS	roan.ns.cloudflare.com.
$ dig +nocmd @8.8.8.8 and.guide NS +noall +answer
and.guide.		21600	IN	NS	frida.ns.cloudflare.com.
and.guide.		21600	IN	NS	roan.ns.cloudflare.com.
$ dig +nocmd and.guide NS +noall +answer
and.guide.		3600	IN	NS	frida.ns.cloudflare.com.
and.guide.		3600	IN	NS	roan.ns.cloudflare.com.

Same two names, three different lifetimes:

  • 86400 from Cloudflare with the aa flag. Cloudflare documents 24 hours as the default nameserver TTL and lets only Enterprise zones with Foundation DNS change it. Keep that in mind if you ever move away from Cloudflare.
  • 86400 from 1.1.1.1, our zone’s value, not the registry’s 3600.
  • 21600 from 8.8.8.8, six hours. Google’s FAQ says its cache lifetimes are “generally limited to six hours.”
  • 3600 from the ISP resolver, the same as the parent’s value. About three minutes later all three resolvers returned the same numbers again, so this is how each one treats the record, not a lucky moment in a countdown.

For planning, assume an old nameserver can keep receiving queries for the longer of two values: the TLD’s delegation TTL, and the NS TTL your old provider publishes at the zone apex. Resolver caps may shorten that, but you can’t count on them. If your old provider lets you, lower its apex NS TTL a few days ahead, and consider changing the old zone’s apex NS records to the Cloudflare names right after the switch, so resolvers that ask the old servers learn the new set. The propagation guide explains the same countdown for ordinary records.

Export the old zone; don’t rely on the scan

Cloudflare’s onboarding can scan for records, but its documentation says the quick scan works from “a list of recurring patterns” of record names and types seen in other zones. It expects to miss very specific hostnames and custom DKIM selectors such as this._domainkey, and tells you to “always review your DNS records and manually add any missing ones before changing your nameservers.”

The underlying problem is that DNS has no “list everything” query for outsiders. A scanner can only ask about names it guesses, and zone transfers are normally refused.

Live capture, 3 October 2026 (03:48 UTC). Asking one of our own Cloudflare nameservers for a full zone transfer:

$ dig +nocmd @roan.ns.cloudflare.com and.guide AXFR
; Transfer failed.

Your old provider’s export or zone file is the only complete source. When you read it, look in particular for:

  • Names nobody would guess: service hostnames, old campaign names, anything with numbers in it.
  • Wildcards (*): a wildcard answers for every name a scanner tries, so a scan can’t tell it apart from many separate records. Recreate it as one wildcard record. Our zone has one: a DNS-only wildcard pointing at our self-hosted deployment server for our own apps; the wildcard subdomains guide shows how it behaves.
  • TXT records on unusual names: domain-verification tokens, _acme-challenge records, and DKIM keys under every selector your mail services use.
  • SRV, CAA, and other less common types, plus any record already set to a short TTL, which often means someone was mid-change.

If the export is a BIND zone file, Cloudflare can import it (limit 256 KiB). The import page has a Proxy imported DNS records option that applies unless you clear it, and Cloudflare says proxying is on by default when you onboard through the dashboard. A per-record comment tag, ; cf_tags=cf-proxied:false, overrides that option for a single line. Use the tag, clear the option, or fix the records afterwards for anything that must stay DNS only: hosts named in MX records, verification CNAMEs, and non-HTTP services. MX and TXT records can never be proxied. The Cloudflare DNS setup guide has the full proxy-status decision table.

Review the pending zone like a production zone

After you add the domain, the zone sits in Pending Nameserver Update. Cloudflare says it “responds to DNS queries for pending zones on the assigned Cloudflare nameserver IPs” but won’t proxy traffic yet, which is what makes testing before the switch possible. Its proxy documentation adds that until activation, even proxied records behave as DNS only and return your origin’s address. A Free-plan zone that stays pending for 28 days is deleted.

Work through the record list:

  1. Apex and www. These are the records the scan finds most reliably, but confirm they point where your site is served today.
  2. Email. MX, SPF, DMARC (_dmarc), and every DKIM selector. A missing DKIM key fails signatures quietly rather than bouncing mail.
  3. CAA. If you publish CAA records and Universal SSL is on, Cloudflare adds CAA records for the CAs it uses, and they don’t appear in the dashboard. Make sure your own CAA set still allows the CA your origin uses; the CAA guide covers the format.
  4. Proxy status on every A, AAAA, and CNAME record, as above.

Compare old and new answers before you switch

Query the new nameservers directly with +norec. You get Cloudflare’s answer from the zone as it stands, regardless of what any registry or resolver says.

Live capture, 3 October 2026 (03:47 UTC). Asking both of our assigned Cloudflare nameservers about the apex. The MX target and TXT contents are left out on purpose; only the flags, preference, and record counts are shown:

$ dig +nocmd @roan.ns.cloudflare.com and.guide MX +norec | grep -E "flags:|status:"
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 33979
;; flags: qr aa; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 2
; EDNS: version: 0, flags:; udp: 1232
$ dig +short @roan.ns.cloudflare.com and.guide MX +norec | awk "{print \"preference\", \$1}"
preference 10
$ dig +nocmd @roan.ns.cloudflare.com and.guide A +norec +noall +answer
and.guide.		300	IN	A	172.66.47.12
and.guide.		300	IN	A	172.66.44.244
$ dig +nocmd @frida.ns.cloudflare.com and.guide A +norec +noall +answer
and.guide.		300	IN	A	172.66.47.12
and.guide.		300	IN	A	172.66.44.244
$ dig +short @roan.ns.cloudflare.com and.guide TXT +norec | wc -l
       2
$ dig +short @frida.ns.cloudflare.com and.guide TXT +norec | wc -l
       2
$ dig +nocmd @roan.ns.cloudflare.com and.guide CAA +norec +noall +comments | grep -E "status:|flags:"
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 2197
;; flags: qr aa; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1
; EDNS: version: 0, flags:; udp: 1232

aa on every reply confirms the server is answering from the zone. Our A records are proxied and the zone is active, so they show Cloudflare addresses. On a pending zone the same query returns the origin address you stored, which should match what the old provider serves. NOERROR with ANSWER: 0 for CAA means the name exists with no CAA records, a valid state.

Checking by hand doesn’t scale past a few names, so script it. This loop asks the old and new servers the same questions, drops the TTL column (TTLs legitimately differ), and prints only a verdict and record counts, so the output is safe to paste into a ticket:

#!/usr/bin/env bash
# Compare answers from two authoritative servers, name by name and type by type.
# Prints match status and record counts only, never the record values.
OLD=${OLD:-ns1.old-dns-host.example}   # a nameserver at your current provider
NEW=${NEW:-ada.ns.cloudflare.com}      # one of the two names Cloudflare assigned
ZONE=${ZONE:-example.com}
NAMES=${NAMES:-"@ www mail _dmarc selector1._domainkey"}
TYPES=${TYPES:-"A AAAA CNAME MX TXT CAA SRV"}

answers() {  # server name type -> sorted answer lines without the TTL column
  dig +norec +noall +answer @"$1" "$2" "$3" | awk '{ $2 = ""; print tolower($0) }' | sort
}

for n in $NAMES; do
  if [ "$n" = "@" ]; then fqdn=$ZONE; else fqdn=$n.$ZONE; fi
  for t in $TYPES; do
    a=$(answers "$OLD" "$fqdn" "$t"); b=$(answers "$NEW" "$fqdn" "$t")
    [ "$a" = "$b" ] && s=same || s=DIFF
    printf '%-4s  %-32s %-5s old=%s new=%s\n' "$s" "$fqdn" "$t" \
      "$(printf '%s' "$a" | grep -c .)" "$(printf '%s' "$b" | grep -c .)"
  done
done

Fill NAMES from your export, not from memory. We have no second provider to compare against, so to show the output format we ran it with our two Cloudflare nameservers as “old” and “new”, over the apex, www, and an invented probe label.

Live capture, 3 October 2026 (03:47 UTC):

$ OLD=roan.ns.cloudflare.com NEW=frida.ns.cloudflare.com ZONE=and.guide NAMES="@ www nscheck-q7x2" ./compare-ns.sh
same  and.guide                        A     old=2 new=2
same  and.guide                        AAAA  old=2 new=2
same  and.guide                        CNAME old=0 new=0
same  and.guide                        MX    old=1 new=1
same  and.guide                        TXT   old=2 new=2
same  and.guide                        CAA   old=0 new=0
same  and.guide                        SRV   old=0 new=0
same  www.and.guide                    A     old=2 new=2
same  www.and.guide                    AAAA  old=2 new=2
same  www.and.guide                    CNAME old=0 new=0
same  www.and.guide                    MX    old=0 new=0
same  www.and.guide                    TXT   old=0 new=0
same  www.and.guide                    CAA   old=0 new=0
same  www.and.guide                    SRV   old=0 new=0
same  nscheck-q7x2.and.guide           A     old=1 new=1
same  nscheck-q7x2.and.guide           AAAA  old=0 new=0
same  nscheck-q7x2.and.guide           CNAME old=0 new=0
same  nscheck-q7x2.and.guide           MX    old=0 new=0
same  nscheck-q7x2.and.guide           TXT   old=0 new=0
same  nscheck-q7x2.and.guide           CAA   old=0 new=0
same  nscheck-q7x2.and.guide           SRV   old=0 new=0

The invented label returns one A record because our wildcard answers for it. Against a real old provider, read the DIFF lines this way:

DIFF on Usually means Action
A, AAAA, or CNAME with equal counts A wrong origin value was imported, or the zone is already active and serving Cloudflare addresses for proxied names Compare with the export; check the zone status
new=0 anywhere Record missing from the Cloudflare zone Add it before switching
old=0 Record exists only at Cloudflare Check it was intended
CAA with more records at Cloudflare Universal SSL’s added CAA records Expected if you publish CAA
TXT with equal counts Long TXT values split into strings differently, or a stale token Compare the values by hand

0 covers both NXDOMAIN and an empty answer, so add +noall +comments to a single query when you need to tell them apart.

DNSSEC: settle it before the nameserver change

If the old provider signs your zone, the registry publishes a DS record that matches the old provider’s key. Cloudflare’s setup guide is blunt: turn DNSSEC off at the registrar before replacing nameservers, because changing them while it’s active “can cause your domain to become unreachable.” Check what the parent publishes:

Live capture, 3 October 2026 (03:44 UTC). A DS query for and.guide at a .guide server:

$ dig +nocmd +norec and.guide DS @v0n0.nic.guide +noall +comments +authority | grep -E "status|flags|SOA|NSEC3"
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 23936
;; flags: qr aa; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1
; EDNS: version: 0, flags:; udp: 1232
guide.			3600	IN	SOA	v0n0.nic.guide. hostmaster.donuts.email. 1790998661 7200 900 1209600 3600

Unlike the NS query, this answer carries aa: the DS record belongs to the parent zone, so the registry answers for it directly. ANSWER: 0 means and.guide has no DS; our zone is unsigned, and a move would have no DNSSEC step. If yours shows a DS, remove it, wait until the parent stops serving it plus the DS TTL, then change nameservers. Re-enable DNSSEC at Cloudflare only after the zone is active. Cloudflare also documents a multi-signer migration that avoids the unsigned gap if your old provider can serve Cloudflare’s key. The DNSSEC guide covers both orders and what the failure looks like.

A switchover timeline

This is a plan template, not a record of a migration we ran. Replace the TTLs with yours; the TTL planner turns them into dates.

When Step Done when
T−7 days Export the old zone; add the domain to Cloudflare; import and review Every record in the export exists at Cloudflare with the right proxy status
T−3 days If signed, remove the DS at the registrar A TLD server returns no DS, and the old DS TTL has passed
T−2 days Lower TTLs on records you’ll change, and the old provider’s apex NS TTL if it allows Old servers serve the lower TTLs
T−1 hour Run the comparison script over every name in the export Only expected DIFF lines remain
T0 Replace the nameservers at the registrar with exactly the two Cloudflare names A TLD server returns the Cloudflare names
T0 + minutes to hours Cloudflare checks the delegation (first after 60 seconds, then at increasing intervals; you can request an earlier check) Zone shows Active
T0 + delegation TTL Resolvers working from the parent’s referral now use Cloudflare 1 hour for .guide; 48 hours for .com
T0 + old apex NS TTL Resolvers holding the old zone’s own NS set have expired it Often 1 to 2 days
T+7 days or later Decommission the old zone; re-enable DNSSEC at Cloudflare; raise TTLs Old servers have stopped receiving queries, if the old provider shows that

Until the decommission row, treat the old zone as frozen and production: any change you make goes into both zones. Some registrars take time to pass a change to the registry, and Cloudflare advises waiting up to 24 hours for that.

After the switch

  1. Ask a TLD server, as in the first capture. If it still shows the old names, the registry hasn’t received the change; Cloudflare’s own troubleshooting starts the same way, with dig +trace example.com NS +noall +authority +nodnssec.
  2. Compare SOA serials on both Cloudflare nameservers. For and.guide at 03:44 UTC both returned 2416226589, so both served the same zone version.
  3. Check resolvers. The DNS lookup tool shows 1.1.1.1 and 8.8.8.8 side by side. An old answer from one resolver while the TLD and Cloudflare agree is a cache waiting out a TTL, not a broken zone.
  4. Test the services, not just DNS. Load the site over HTTPS, send mail to and from the domain, and check any vendor that verified the domain by DNS.
  5. Keep the old zone serving through the last timeline row, then delete it.
dig +nocmd +norec example.com NS @a.gtld-servers.net +noall +authority
dig +nocmd @ada.ns.cloudflare.com example.com SOA +norec +noall +answer
dig +nocmd @bob.ns.cloudflare.com example.com SOA +norec +noall +answer
dig +nocmd @1.1.1.1 example.com NS +noall +answer

If the zone stays pending even though a TLD server already shows the Cloudflare names, look for a leftover DS record or an extra nameserver at the registrar. Cloudflare’s guidance is to list only its two assigned names.