Stage 1: What the parent publishes about you
A registry holds almost nothing about your domain. Three things, and you can write none of them directly:
What the registry actually stores.
- The delegation NS set — the names of the servers resolvers will ask.
- Glue — addresses, but only for nameserver names that sit inside the domain being delegated.
- The DS record — a hash of one of your keys, if you use DNSSEC.
Everything else — your A records, your MX, your TXT, your TTLs — lives at your DNS provider and the registry has never seen it. Your registrar’s control panel is a channel to those three things, not a copy of them. What you typed into the form and what is published are different objects with different lifetimes, and there is no read-back path from one to the other. The only way to see what is published is to ask a parent nameserver, which is what the hinge does.
Here is a real referral, from the lab’s TLD server, with recursion off and DNSSEC on. This is the parent’s entire opinion of the zone, and it fits in one packet:
$ dig +norec +dnssec @127.0.0.11 example.test A +noall +authority +additional
example.test. 3600 IN NS ns1.example.test.
example.test. 3600 IN NS ns2.example.test.
example.test. 3600 IN NS ns3.example.test.
example.test. 3600 IN DS 24922 13 2 BC2049B4...8CA1808C
example.test. 3600 IN RRSIG DS 13 2 3600 20261003215802 20260903215802 48667 test. ...
ns1.example.test. 3600 IN A 127.0.0.21
ns2.example.test. 3600 IN A 127.0.0.22
ns3.example.test. 3600 IN A 127.0.0.23Read it by owner rather than by record type. The NS records decide which servers get asked; the addresses under them are glue, and exist only because those names sit inside the zone being delegated; the DS decides which key of yours is allowed to sign; and the RRSIG proves the DS came from the parent.
Notice which record does not carry a signature. The delegation NS set is not authoritative data, it is not signed, and DNSSEC does not protect it. The child’s own copy of that same-looking NS set is signed, by the child. Two copies of the same list, in two zones, with different cryptographic status and different consumers — and nothing in any protocol, implementation or registry check ever compares them. In this lab they disagreed for hours with nothing logging anything.
The command everybody runs shows you the wrong list
dig example.com NS does not show the delegation. It asks your recursive resolver, which answers from cache with the child’s apex NS set. Here the parent published ns1, ns2, ns3 and the zone published ns1, ns2, ns4:
$ dig +norec @127.0.0.11 example.test NS +short | sort | tr '\n' ' ' # the parent
ns1.example.test. ns2.example.test. ns3.example.test.
$ dig @127.0.0.30 example.test NS +short | sort | tr '\n' ' ' # Unbound
ns1.example.test. ns2.example.test. ns4.example.test.
$ dig @127.0.0.31 example.test NS +short | sort | tr '\n' ' ' # BIND
ns1.example.test. ns2.example.test. ns4.example.test.In the exact case you would run this command to detect — the two lists disagreeing — it shows you the one nobody uses. That is why the hinge asks the parent.
A server the parent does not name receives nothing. Not “less”. Nothing.
This is the belief that makes people think a spare nameserver is insurance. In the lab, multi.test was delegated by the parent to nsa only; the zone’s own NS set named nsa and nsb; nsb was up and answering correctly. Then nsa was stopped:
$ dig +norec @127.0.0.29 who.multi.test +short
203.0.113.29 <- nsb, healthy, authoritative, answering
$ dig @127.0.0.31 who.multi.test | grep status:
;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 29813
$ dig @127.0.0.30 who.multi.test <- Unbound: no answer in six secondsThe packet capture is the whole argument. Every query the resolver sent, in order, with the healthy server’s address appearing zero times:
IP 127.0.0.1.56128 > 127.0.0.31.53: A? who.multi.test.
IP 127.0.0.1.42382 > 127.0.0.10.53: NS? .
IP 127.0.0.1.54872 > 127.0.0.10.53: NS? test.
IP 127.0.0.1.45427 > 127.0.0.11.53: NS? multi.test.
IP 127.0.0.1.36563 > 127.0.0.28.53: A? who.multi.test. <- the dead one
IP 127.0.0.31.53 > 127.0.0.1.56128: ServFail
$ tcpdump -r cap2.pcap -n | grep -c "127.0.0.29"
0It holds with a warm cache too — a resolver that already had a live delegation and a live answer, watching the delegated server die, still never tried the other one. Adding a nameserver to your zone does not add a nameserver. It adds a name to a list used for one thing only — deciding who gets a NOTIFY, which is stage 2.
And the order is the part nobody states. The new server must already hold the zone before the delegation changes. Add it at the registrar first and you have delegated to a server that answers REFUSED, which resolvers treat as lame and which costs you a share of your queries for the whole of the parent’s TTL. Add it to your zone first and it gets NOTIFY but no traffic. Both lists, and the zone loaded before the registrar form is submitted.
Glue, and the two cases nobody names
Everyone knows glue is needed when a nameserver’s name is inside the zone it serves. There are two neighbours, both real, and one of them is how two of your own domains take each other down:
$ dig +norec @127.0.0.11 example.test A +noall +authority +additional
example.test. 3600 IN NS ns1.example.test.
ns1.example.test. 3600 IN A 127.0.0.21 <- IN-DOMAIN glue: required
$ dig +norec @127.0.0.11 sibling.test A +noall +authority +additional
sibling.test. 3600 IN NS ns1.example.test.
ns1.example.test. 3600 IN A 127.0.0.21 <- SIBLING glue: optional
$ dig +norec @127.0.0.11 external.test A +noall +authority +additional
external.test. 3600 IN NS ns.hoster.net. <- OUT OF BAILIWICK: no glue possibleSibling-domain glue is optional — the parent may send it and often does, and some resolvers will not use it even when sent. If a.example and b.example point at each other’s nameservers, you are relying on a courtesy. Out-of-bailiwick names cost a second full resolution, which can itself fail. And RFC 9471 (2023) changed the rules for the required case: a server must return all in-domain glue, and must set the truncation bit if it will not fit rather than quietly dropping records — which matters because the shipped EDNS buffer size is now 1232 bytes.
Verify: run the hinge command. The NS set it returns should be exactly the set in your zone, every in-domain name should have an address beside it, and the DS — if you expect one — should be there with an RRSIG. Anything else is stage 1, and you fix it at a registrar rather than in a zone file.
Stage 2: The zone, the serial, and the copies
Your zone file is not the zone. It is one of the inputs to one of the copies, and the copy everybody reads is usually on a machine you do not own. The only thing that connects the copies is a single 32-bit number in the SOA record, and almost everything people believe about that number is wrong.
It is not compared with >. It is compared with RFC 1982 serial arithmetic, in which “newer” means within half the number space — so serial 5 is newer than 4294967290, and a serial you thought was a big increase can be a decrease. BIND will print the legal window at you if you get it wrong, which is the clearest statement of the rule any program produces:
$ rndc reload example.test
zone reload queued
$ tail -2 named.log
zone example.test/IN: ixfr-from-differences: new serial (2026090302) out of range [2026090303 - 4173573949]
zone example.test/IN: not loaded due to errors.rndc reload reported success and the zone did not reload. That upper bound is the serial plus 231−1, printed on the screen. The most common consequence is not an outage but a silent no-op, which the operator then experiences as slow propagation.
A serial that goes backwards cannot be undone
Restoring a zone file from a backup is offered as a rollback everywhere. It is not one. Two separate disasters, both reproduced:
The primary refuses to load its own zone, and says it is fine. With a journal present, an older serial makes the load fail — and named logs all zones loaded, logs running, exits 0, keeps serving its other zones, and answers SERVFAIL with no aa bit for this one:
$ dig +norec @127.0.0.21 example.test SOA
;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 19203
;; flags: qr; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1
$ grep -E "journal|not loaded|all zones loaded|running" named.log
zone example.test/IN: journal rollforward failed: journal out of sync with zone
zone example.test/IN: not loaded due to errors.
all zones loaded
runningAnd with the journal removed, it loads — and every secondary correctly refuses it. Both implementations, in their own words, both right:
BIND: zone example.test/IN: serial number (2026090259) received from primary
127.0.0.21#53 < ours (2026090309)
Knot: [example.test.] refresh, remote 127.0.0.21@53, remote serial 2026090259,
remote is outdatedThe zone is now permanently split, with the primary holding old data and the secondaries holding new, and no amount of waiting fixes it — the protocol is working exactly as designed. The escape is to move the serial forward past the highest one anywhere, not backwards to where you were.
NOTIFY is an optimisation, and its address list is not in your config
The mechanism that keeps secondaries current is the SOA REFRESH timer, and NOTIFY only shortens the wait. Worth knowing because REFRESH is clamped by the secondary, so a low value in your zone file does nothing at all — BIND’s built-in bounds, from the binary itself, are min-refresh-time 300 and max-refresh-time 2419200, with retry clamped to 500 and 1209600 seconds.
And the list of who receives a NOTIFY is not something you configure. It is the zone’s own apex NS set, minus the server named in the SOA. With notify yes and no also-notify anywhere in the configuration, two packets went out, to the two other servers the zone names:
23:42:44.200005 IP 127.0.0.21.32969 > 127.0.0.22.53: notify SOA? example.test.
23:42:44.699905 IP 127.0.0.21.51445 > 127.0.0.23.53: notify SOA? example.test.This is the exact mirror of the hinge, and it is the sentence to keep. Query traffic comes from the parent’s list; NOTIFY comes from the child’s. A server missing from one is broken in a completely different way from a server missing from the other — invisible and starved in the first case, delegated and stale in the second. And what arrives when a secondary does ask is not reliably a delta: RFC 1995 lets a server answer an IXFR request with the whole zone, and BIND does so whenever the delta would exceed max-ixfr-ratio — which, on a signed zone, is nearly every time, because re-signing regenerates every RRSIG.
The second hinge, for this stage. Ask every server the parent named for the SOA, and read two things — whether a line came back at all, and how the serials compare:
for ns in $(dig +norec @"$PARENT" "$DOMAIN" NS +short); do
printf '%-28s ' "$ns"; dig +norec @"$ns" "$DOMAIN" SOA +short
done- A blank line. That server is in the delegation and is not serving the zone. This is the branch nobody checks, and it is the worst one.
- Serials differ, and the secondary’s is lower. Ordinary lag. Find out whether the NOTIFY reached it.
- Serials differ, and the secondary’s is higher. The primary went backwards. They will never converge on their own.
- Serials all equal, and the data still differs. Two zones wearing the same number. Somebody edited a secondary.
Verify: run the second hinge. Every server the parent named should return a serial, and they should all be the same one. If a server returns nothing, stop reading and go to stage 3 — it is in the rotation.
Stage 3: Which copy answers
Here is what the previous two stages are for. After a serial roll and one subsequent edit, the three servers the zone named were in two different states, and all three set the authoritative-answer bit:
SERVER SERIAL www.example.test flags
127.0.0.21 6 203.0.113.55 qr aa
127.0.0.22 6 203.0.113.55 qr aa
127.0.0.23 2026090309 203.0.113.77 qr aaNo error anywhere. No log line anywhere. Two different answers to the same question, both authoritative. Forty queries through the two resolvers, with the cache flushed between each:
Unbound: 20 203.0.113.77
BIND: 11 203.0.113.55
9 203.0.113.77BIND is a coin flip. Unbound went to the stale server twenty times out of twenty — consistently, not randomly, which is the more frightening result. The likely mechanism is its infrastructure cache preferring the server with the best measured round-trip time, and the best server happening to be the wrong one; that part is a probable explanation rather than an instrumented one, and the ratio is what was measured.
Now the operator, at a shell, running the check that every guide on the subject recommends:
$ dig @127.0.0.30 www.example.test +short # what the world is getting
203.0.113.77
$ dig +norec @127.0.0.21 www.example.test +short # what the operator checks
203.0.113.55dig @your-nameserver is the most common verification step in the whole subject, and it is the one query that cannot detect any of the four commonest failures — a wrong delegation, a diverged secondary, a broken DS, and a cache that has not expired. It asks a machine you chose, and the resolvers do not.
The bit nobody reads, and the failure it names
aa is in every response and it distinguishes this server holds your zone from this server exists. A delegated server that has lost the zone answers without it, and that is what “lame” means. It is not a degraded state. Delegate a name to two servers, black-hole one of them and leave the other answering non-authoritatively, and:
BIND 20.02 s, then SERVFAIL
Unbound no answer at all inside 60 s
named.log: lame server resolving 'www.halflame.test' (in 'halflame.test'?): 127.0.0.11#53A fully black-holed single-server delegation behaves the same way — twenty seconds to SERVFAIL from BIND, nothing at all from Unbound. A server that refuses the connection is dealt with in 0.02 seconds, so a firewall that drops packets is very much worse than a nameserver that is switched off. BIND’s lame-ttl now defaults to 0, which means it no longer remembers that a server was lame and re-learns it on every query.
Verify: for every server the parent named, dig +norec @$ns $DOMAIN SOA +noall +comments and look for aa in the flags line. Not for an answer — for the flag. A server can return a perfectly plausible record without it.
Stage 4: The chain the parent vouches for
The DS record is a hash of one of your keys, published by a stranger, and nothing ever checks it against your zone. Two things follow that most pages get backwards. Signing is entirely the child’s act: a zone with no DS is still a signed zone, and every resolver simply ignores the signatures — the answer comes back without the ad bit and with no complaint from anybody. The DS does not make the signatures exist. It makes them checkable, and therefore mandatory.
Which is why the failure mode here is total and invisible at the same time. In the lab the parent published a DS for key tag 24922 while the zone signed with 58884. All three authoritative servers answered in milliseconds:
$ for ns in 127.0.0.21 127.0.0.22 127.0.0.23; do dig +norec @$ns www.example.test +short; done
203.0.113.77
203.0.113.77
203.0.113.77
$ dig @127.0.0.30 www.example.test | grep -E "status:|EDE"
;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 60501
; EDE: 6 (DNSSEC Bogus): (validation failure <www.example.test. A IN>: no keys have
a DS with algorithm ECDSAP256SHA256 from 127.0.0.22 for key example.test.
while building chain of trust)There is no aa check, no HTTP monitor and no dig @ns1 that sees this. The only thing that sees it is a validating resolver — which is to say, a large share of your users and none of your dashboards. The extended error is worth having: RFC 8914 carries the reason in the response, and Unbound 1.19.2 ships with ede: no, so turning it on converts an opaque SERVFAIL into the sentence above. BIND 9.18.39 returned no extended error at all for a bogus answer in these tests, and did return one for a stale answer. Coverage is partial in both, and version-dependent.
The ordering rule, which is the opposite of the one people use
Because the DS lives at the parent and carries the parent’s TTL, every DNSSEC change is two changes on two clocks, and they must overlap. Publish the new DS alongside the old one, wait a full DS TTL, move the signing, and only then remove the old DS. Never the sequence “remove, then change” — that is the shape that takes a domain off the internet for everyone who has already cached the old DS, and it is the shape our own guide recommends.
The better answer is to stop doing it by hand. You can publish your own DS material in your own zone and let the registry take it from there — CDS and CDNSKEY records, standardised in RFC 7344 with the acceptance rules in RFC 8078, the bootstrapping problem solved by RFC 9615, and a way for your servers to tell the parent to come and look added by RFC 9859. And the newest piece is the one nothing else on this subject mentions: RFC 10026, published 23 July 2026 as BCP 246, tells registries to drop the DS TTL to five or fifteen minutes around a change and hold it there for a couple of days — an acknowledgement, in a best-current-practice document, that the clock nobody can set was the problem all along.
One sentence on the event you will have seen in the news, because it is not yours: on 11 October 2026 KSK-2024 becomes the key signing the root zone. It changes the trust anchor that validating resolvers carry. It requires nothing from a zone owner — no re-sign, no new DS, no rollover — and any page telling you to prepare your zone for it has confused the root’s key with yours. The resolver-operator side is DNS, All the Way Down‘s.
Verify: delv @<a validating resolver> $DOMAIN A either prints fully validated or names the link that failed. Then compare the key tag in the parent’s DS — the first number in it — with dig +norec @$ns $DOMAIN DNSKEY | grep 257 piped through dnssec-dsfromkey -f -. Two numbers, and they either match or your domain is off the internet for everyone behind a validating resolver.
Stage 5: The clocks
Everything above happens instantly. What takes time is other people’s caches, and the corpus models that with a single number — the TTL you set — which is where nearly all of the remaining trouble lives. There are six clocks. Two of them are yours, and neither of those two governs either of the changes that actually take a domain down.
Start with the one this site got wrong. A negative answer — the “no” a resolver caches when a name does not exist yet — lasts for min(SOA MINIMUM, the TTL of the SOA record itself, the resolver's own negative cap). DNS, All the Way Down states the first two terms correctly and then says you need to lower both. It is a minimum: lowering either one is sufficient. Measured, with $TTL left untouched at 3600 and MINIMUM alone dropped to 60:
$ dig +norec @127.0.0.21 example.test SOA +noall +answer
example.test. 3600 IN SOA ns1.example.test. hostmaster.example.test. 2026090304 3600 900 1209600 60
$ dig +norec @127.0.0.21 stillnothere.example.test A +noall +authority
example.test. 60 IN SOA ns1.example.test. hostmaster.example.test. 2026090304 3600 900 1209600 60Same record, same server, same second, two different TTLs — because the authoritative server applies the first min() itself before putting the SOA into a negative answer. Both resolvers then cached the “no” for sixty seconds. The number you read depends on which question you asked, which is a good enough reason on its own to look at the authority section rather than the answer section.
| The clock | Who sets it | What it delays | Where you read it |
|---|---|---|---|
| The record’s TTL | you | a changed A, MX or TXT record | your zone file |
| The negative TTL | you, then the resolver | a name that did not exist when somebody asked | the authority section of an NXDOMAIN |
| The delegation NS TTL | the registry | every nameserver change | the hinge command, NS rows |
| The DS TTL, plus the validator’s bogus TTL | the registry, then the resolver | every DNSSEC change | the hinge command, DS row |
| The resolver’s ceiling and floor | whoever runs the resolver | everything you publish | you cannot — Unbound caps at 86400 positive and 3600 negative, BIND at 604800 and 10800 |
| The stale window | whoever runs the resolver | everything, after its TTL has expired | you cannot — up to 86400 where it is on |
That table is a table because the values are documented software defaults, printable from named -C and from unbound-control get_option. A table of per-TLD delegation TTLs would be somebody else’s machine state, so this page does not have one — read your own with the hinge.
One zone with $TTL 604800 was cached by Unbound for 86400 and by BIND for 604800 — four different lifetimes for one record once the negative caps are counted, all of them stock defaults. The number in your zone file is a request. And serve-stale means an expired record can still be served for up to a day (RFC 8767). Both of the resolvers tested ship it off; the large public resolvers your users are actually behind do not — so “wait one TTL” was never the rule for anybody. BIND needs two settings rather than one, which is the trap for anyone turning it on deliberately: stale-answer-enable yes alone does nothing, because stale-cache-enable defaults to false and the expired record was never kept. Ask it and it says so — stale cache disabled; stale answers unavailable. With both on, a sixty-second record came back sixty-two seconds after it expired, carrying EDE: 3 (Stale Answer).
Verify: ask the resolvers your users actually use, without recursion, and read the TTL rather than the answer — dig +norecurse @1.1.1.1 www.example.com. A counting-down number is a cached copy and tells you how long is left. No answer at all means that resolver has not looked it up, which is the only completely good news available at this stage.
Stage 6: When the change is over
This is the section every page on this subject stops one short of, and the reason is in the model: if publishing a document is the mental picture, then saving the file is the end of the story. It is not. There is no global check, there never will be, and the only real evidence a change is finished is negative — the old servers stop being asked.
The two clocks that matter here are both at the parent and both are measurable rather than folkloric. A nameserver change takes effect at exactly the parent’s delegation NS TTL — not the answer’s TTL, not your zone’s own NS TTL. In the delegation-change run the answer’s own TTL was 30 seconds, the zone’s own NS TTL 3600, and the parent’s delegation TTL 120 — and both resolvers switched at 120:
elapsed Unbound BIND
0s 203.0.113.28 203.0.113.28
...
105s 203.0.113.28 203.0.113.28
120s 203.0.113.29 203.0.113.29A DNSSEC fix takes effect at the DS TTL plus the validator’s own bogus-answer timer, and those are two different numbers in two different places. With a 90-second DS TTL, a fix that was live on the authoritative servers at t=0 reached BIND at exactly 90 seconds and Unbound at 140 — the extra 50 being Unbound’s val-bogus-ttl, which defaults to 60:
0s Unbound=SERVFAIL BIND=SERVFAIL
90s Unbound=SERVFAIL BIND=203.0.113.10
140s Unbound=203.0.113.10 BIND=203.0.113.10The fix was correct and complete before anyone could see it, and it arrived at different people at different times. Ninety seconds and a hundred and forty are the lab’s numbers, because the lab’s DS TTL was ninety. At a real registry that first number is set by the registry, is usually measured in hours rather than seconds, and is one you read rather than choose — the hinge command prints it. That is the whole shape of a DNS incident: it does not start and it does not end, it spreads and it drains.
The question for this stage: of the six clocks, which ones were yours?
If the answer is “the record’s TTL and the negative TTL, and I lowered them in advance”, you have done everything available to you and everything remaining is somebody else’s number. So go and read them rather than waiting on them: the parent’s, with the hinge command; the resolvers’, with dig +norecurse against the ones your users are behind. And watch the old nameservers’ query log, not the old web server’s — the query log is the only place in the entire system where “finished” is observable, because it is the only place the absence of a question is recorded.
Verify: when the old nameservers have had no queries for your zone for longer than the parent’s delegation TTL, the change has reached everyone who was going to ask. That is as close to “done” as this system offers, and it is why the guides that tell you to leave the old servers running are right for a reason they do not give: they are your only instrument.
A worked diagnosis: the Friday migration
A company moves DNS providers. The new provider’s zone is byte-identical to the old one, checked record by record with dig @ns1.newprovider.net. The nameservers are changed at the registrar on Friday afternoon. The old provider is left running, as every guide including this site’s own advises.
Over the weekend, tickets arrive from customers who cannot reach the site. Not all of them, and not the same ones twice. The site is up. Monitoring is green. dig from three different laptops is fine. dig @1.1.1.1 is fine. Both sets of nameservers are answering. Traffic on the old servers is falling exactly as expected.
The hinge, run once:
$ PARENT=$(dig +short NS com. | head -1)
$ dig +norec +dnssec @"$PARENT" example.com A +noall +authority +additional
example.com. 172800 IN NS ns1.newprovider.net.
example.com. 172800 IN NS ns2.newprovider.net.
example.com. 86400 IN DS 41287 13 2 <the OLD provider's key>
example.com. 86400 IN RRSIG DS 13 2 86400 ...The NS set is right. The DS is not — it fingerprints the old provider’s key signing key, and the new provider signs with its own. Nobody removed it, because nobody at either provider owns it: the DS lives at the registry, and the only party who can change it is the registrant, through a form most people have never opened.
The reason it is intermittent rather than total is the second half of stage 6. Resolvers pick up the new delegation as their own copy of that referral expires, and in this story that referral’s TTL is 172800 — two days. Yours is whatever your own registry publishes, and the hinge command is where you read it. So the failure does not arrive; it spreads, resolver by resolver, across a weekend, and the population affected is precisely the population behind a validating resolver.
And the obvious fix makes it worse. “Turn DNSSEC off” — remove the DS — is what our own guide says, and it buys another 86400 seconds of the identical failure for everyone who has already cached the bad one. The correct sequence is the one RFC 10026 spends a document on: publish the new DS alongside the old, wait a DS TTL, then remove the old one — or hand the registry your CDS record and let it do the ordering for you.
Note where the hinge points, and where it stops. Its DS branch says stage 4, and stage 4 tells you the DS is wrong. It does not tell you why the failure is intermittent, and it does not tell you when it ends — both of those are stage 6, and an engineer who fixes the DS and goes home on Saturday gets the same tickets on Sunday. The hinge is a localiser, not an oracle, and the reason it is worth having anyway is that it converts “DNS is broken somehow” into one of six sections in one command.
The operator in this story owned every byte of both zones, verified both, and was wrong about neither. The two things that were broken were published by a party they had never spoken to, on behalf of a provider they had just left.
The moral, and it is the page in one line: your parent publishes two facts about your zone — who serves it, and which key is allowed to sign it — and it has never checked either one against your zone.
Symptoms, and which stage owns them
| What you see | Stage | What to run, or what it is |
|---|---|---|
| A new nameserver is live and receiving no traffic at all | 1 | it is in your zone and not in the parent’s delegation |
| A nameserver you removed is still being queried | 1, 6 | the parent’s delegation NS TTL, which you did not set |
| The domain is dead for some resolvers and fine for others, with every server healthy | 4 | the DS does not match the key in the zone; delv names the link |
Two of your nameservers give different answers, both with aa | 2, 3 | compare serials; then find out which one your resolver prefers |
| A secondary’s serial is higher than the primary’s | 2 | the primary went backwards; they will never converge |
rndc reload succeeded and the change is not visible anywhere | 2 | the serial is not greater under RFC 1982; the zone did not load |
| Resolution takes twenty seconds and then fails | 3 | a lame or black-holed server in the delegation |
| You added a record and it still does not exist | 5 | the negative answer cached before you added it |
| A record is still being served after its TTL expired | 5 | serve-stale, for up to a day |
dig $DOMAIN NS disagrees with your registrar | 1 | it is showing the child’s list; the parent’s is the one used |
| Everything above is correct and a resolver still gives the old answer | 6 | nothing is broken; read the clocks rather than waiting on them |
Advice on this subject that has expired
| Commonly said | What is actually true |
|---|---|
| “Turn DNSSEC off, then migrate” | Removing the DS is a change to the parent’s zone on the parent’s TTL. Publish the new DS alongside the old one instead, or use CDS. |
| “Lower the TTL and wait one TTL” | True only for record changes. A nameserver change runs on the parent’s delegation TTL and a DNSSEC change on the DS TTL, and you set neither. |
| “Wait one TTL and the old answer is gone” | Serve-stale (RFC 8767) lets a resolver serve an expired record for up to a day, and the large public resolvers do. |
“dig example.com NS shows the delegation” | It shows the child’s apex NS set from your resolver’s cache — the list nobody uses when the two disagree. |
| “IXFR is incremental” | RFC 1995 lets a server return the whole zone in AXFR format, and BIND does whenever the delta exceeds the zone. On a signed zone that is nearly every time. |
| “CNAME is forbidden at the apex” | RFC 2181 §10.1 forbids it beside other data at any name. The standards-track answer for the apex is now RFC 9460 HTTPS/SVCB. |
“Leaving allow-transfer unset is safe” | BIND 9.18’s built-in default is allow-transfer {any;}, printable from named -C. Knot refuses by default. Two of the most common servers, opposite defaults. |
| “DS changes have to be done by hand at the registrar” | RFC 7344, 8078, 9615 and 9859 automate the whole chain from your own zone, and BCP 246 (23 July 2026) tells registries how to time it. |
| “Prepare your zone for the root KSK rollover” | KSK-2024 becomes active on 11 October 2026 and requires nothing at all from a zone owner. |
How to tell whether a page about DNS is worth reading
Every error above comes out of one model, and naming it is more useful than the list. The corpus models a DNS zone as a document you publish, with the TTL as the publication delay. One document, one publisher, one clock, and the clock is yours. Watch it generate the rest:
- “Propagation.” Publishing is a push, so the change must travel — hence the pages that describe ISPs updating their records. Nothing updates anything and nobody is pushed to.
- “Lower the TTL and wait one TTL.” One clock, and it is yours, so the procedure is complete. It misses the four you cannot edit, two of which are the ones that govern a nameserver change and a DNSSEC change.
- The document is the zone, so reading it is proof — hence
dig @ns1.provider.netas the verification step, asked of a machine the resolvers were never going to consult. - The document lists its servers, so listing a server adds one — hence “add the nameserver to your zone”, with the registrar treated as paperwork. Measured: zero packets.
- Publishing is instantaneous, so unpublishing is too — hence “turn DNSSEC off, then migrate”.
- A published document is finished, so the page has no ending. This is the structural one, and it is why this article has a stage 6 and almost nothing else does.
The one-question test: does the page name a single TTL the domain owner cannot edit? There are four — the parent’s NS TTL, the parent’s DS TTL, the resolver’s cache ceiling, and the resolver’s stale window. If every number on the page is one you set in your own zone file or your own control panel, the author has only ever changed an A record and has never watched a change fail to take.
What the silences prove
- No mention of the serial, or of what a higher serial on a secondary means — they have only ever used a web control panel.
- No mention of NOTIFY, AXFR or IXFR — they have run exactly one nameserver.
- No mention of
aain a dig response — they have never had a delegated server stop serving a zone, and so have never needed the one bit that tells them apart. - “Flush your DNS cache” offered as the fix for a change not taking effect — they are describing their own laptop and have generalised it to the internet.
A page that has visibly fixed itself, and the half it did not absorb
The honest example is our own. Getting a Domain and Its DNS Right already carries the correction that most of the internet has not made — “lowering the TTL on the day of the move accomplishes nothing, because the resolvers holding the old record also cached the old TTL along with it”. That is the push model demolished and replaced with independent expiry, including the subtle part.
And then it names not one TTL the reader does not control. The absorbed half is “there is no push”. The un-absorbed half is “there is more than one clock, and you set none of the ones that matter” — which is this page’s entire subject, and which is why the same page’s DNSSEC instruction is one clause short. A correction that stops at the mechanism leaves the reader with a better model of a system they still cannot see into.
Before you call it done
- Run the hinge against your own domain and read the NS set, the glue and the DS.
PARENT=$(dig +short NS "${DOMAIN#*.}" | head -1), thendig +norec +dnssec @"$PARENT" "$DOMAIN" A +noall +authority +additional. - Write down the parent’s NS TTL and DS TTL. They are in that response, they are the two numbers that decide how long your next change takes, and you cannot edit either.
- Ask every server the parent named for the SOA, and check that each one answers, sets
aa, and returns the same serial. - If you are signed, compare the DS key tag with your own key —
delvagainst a validating resolver is the one-command version. - Set your SOA MINIMUM to something you would be happy to wait for, and remember it bounds only the “no” — and that lowering either it or the SOA record’s own TTL is enough.
- Before any nameserver or DNSSEC change, decide the order and write it down: the new server holds the zone before the delegation moves, and the new DS is published before the old one is removed. Both are the same rule — overlap, never replace.
Related reading
- DNS, All the Way Down — the eight stages on the client’s side of the same wire, and the page this one hands the negative-caching arithmetic back to.
- Getting a Domain and Its DNS Right — buying the domain and choosing the records, and the page whose DNSSEC instruction this one exists to finish.
- Server Migration — the other half of the weekend this page’s worked diagnosis happens on.
