Module 03 — Delegation, Recursion & Caching

Updated 20 August 2026

Module 03 · Delegation, Recursion & Caching

Module 01 said a recursive resolver "does the work". Module 02 said an NS record hands authority over. This module is the machinery in between — how a resolver walks from the root to an answer, what it stores along the way, and why a change you made an hour ago is still invisible to half the world.

🧠 concept → 🧪 exercise → ✅ expected result (hidden) → 🎯 interview questions (answers hidden)

Prerequisite: Modules 01 and 02. You need NS and SOA records, the aa flag, NXDOMAIN vs NODATA, and how to read a dig header.


Part A · How a resolver actually finds an answer

A1 · Where the walk starts: root hints

The analogy. Think of the one phone number written inside the cover of your notebook. You cannot look up the phone book's number in the phone book — so one number gets printed on the cover.

You call it once, and the first thing you ask for is the current, correct list. The printed number only has to be good enough to make that first call, which is why it can be years out of date and everything still works.

A resolver starting from nothing has a bootstrapping problem: to look anything up it must ask a server, but to find a server it must look something up.

The problem is solved by not solving it — the answer is shipped in the software. Every recursive resolver ships a small file, conventionally called named.root or root hints, containing the names and IP addresses of the 13 root server identities. That file is the only DNS data a resolver is given; everything else it learns.

Why they are called "hints" and not "configuration". The file is not treated as authoritative. On start-up the resolver uses it to contact any one root server, then asks that server for the current root NS set and caches the real answer. The file only has to be good enough to get one packet through. That is why it can be years out of date and everything still works — a design decision that has quietly saved the internet from a great deal of pain.
🧪 Exercise A1.1 — Fetch the root zone's own view of itself
bash
dig . NS +noall +answer | head -4
dig +short a.root-servers.net A
dig +short a.root-servers.net AAAA
Expected result — click to reveal
plain text
.			87203	IN	NS	a.root-servers.net.
.			87203	IN	NS	b.root-servers.net.
.			87203	IN	NS	c.root-servers.net.
.			87203	IN	NS	d.root-servers.net.

198.41.0.4
2001:503:ba3e::2:30

The owner name is a single dot. That is the root, whose label is the empty string — Module 01 A3 in its most literal form. The root zone is a real zone with a real NS RRset, and you can query it like any other.

The TTL of 87203 tells you something useful: it is a counted-down value, so your resolver learned this from the root itself and is ageing it. The published value is 518400 — six days. Root data is cached aggressively because it changes almost never.

Every root server has both an IPv4 and an IPv6 address, and both are in the hints file. On an IPv6-only network the v4 addresses are unusable, which is why the hints file carries both and why a resolver on such a network still bootstraps cleanly.


A2 · The referral — what a server says when it is not authoritative

The analogy. Think of "not my department — try the third floor". Nothing went wrong. Nobody was unhelpful. You did not get your answer, but you got one step closer, and that is a successful outcome.

The trick is telling that apart from "this is my department, and we have nothing for you" — which sounds similar and means something completely different.

In Module 01 C4.1 you queried a root server and got this, and I promised to explain it later:

plain text
;; flags: qr rd; QUERY: 1, ANSWER: 0, AUTHORITY: 13, ADDITIONAL: 27

That is a referral. The root server did not answer your question and did not fail. It said: I do not hold that data, but I know who is closer — here they are.

Part of the referralWhat it contains
status: NOERRORNothing went wrong. A referral is a successful response
ANSWER: 0No data for your question. The referral is not an answer
No aa flagThis server is not authoritative for the name you asked about
AUTHORITY sectionThe NS records of the child zone — go and ask these
ADDITIONAL sectionAddresses for those nameservers, where the parent has them. This is glue — A4
A referral and a NODATA answer look almost identical, and telling them apart is a real diagnostic skill. Both are NOERROR with ANSWER: 0 and something in the AUTHORITY section. The difference is what is in that section:
  • AUTHORITY contains NS records → referral. "Not me, ask them."
  • AUTHORITY contains an SOA record → NODATA. "It is me, and there is nothing here."

One record type, two completely different meanings. When you are debugging a delegation, this is the distinction you are reading for.


A3 · The whole walk, with dig +trace

The analogy. Think of walking the building yourself instead of sending an assistant. Reception sends you to the department, the department sends you to the right desk, the desk answers.

+trace is you doing that walk in person — which means nobody's memory of a previous visit can mislead you. That is exactly why it is the first command to run when an answer is disputed.

dig +trace does what a recursive resolver does, and prints every step. It is the single most educational command in DNS.

One thing to understand before you read the output: +trace is not asking your resolver to show its work. It bypasses your resolver entirely and performs the iterative walk itself, starting from the root hints. That is exactly why it is so useful — it shows you the truth from the source, unaffected by any cache between you and the data.

It also means +trace can succeed while normal resolution fails, and vice versa. That divergence is a diagnosis in itself: it tells you the fault is in your resolver, not in the zone.

🧪 Exercise A3.1 — Watch the tree being walked
bash
dig +trace seamless.se
Expected result — click to reveal
plain text
; <<>> DiG 9.18.39 <<>> +trace seamless.se
;; global options: +cmd
.			518400	IN	NS	a.root-servers.net.
.			518400	IN	NS	b.root-servers.net.
         ... 13 in total ...
.			518400	IN	RRSIG	NS 8 0 518400 20260901050000 ...
;; Received 525 bytes from 127.0.0.53#53(127.0.0.53) in 4 ms

se.			172800	IN	NS	a.ns.se.
se.			172800	IN	NS	b.ns.se.
se.			172800	IN	NS	c.ns.se.
se.			172800	IN	NS	f.ns.se.
se.			172800	IN	NS	g.ns.se.
se.			172800	IN	NS	i.ns.se.
se.			172800	IN	NS	m.ns.se.
se.			172800	IN	NS	x.ns.se.
se.			172800	IN	NS	y.ns.se.
se.			172800	IN	NS	z.ns.se.
se.			86400	IN	DS	...
se.			86400	IN	RRSIG	DS 8 1 86400 ...
;; Received 771 bytes from 198.41.0.4#53(a.root-servers.net) in 12 ms

seamless.se.		86400	IN	NS	ns-135.awsdns-16.com.
seamless.se.		86400	IN	NS	ns-722.awsdns-26.net.
seamless.se.		86400	IN	NS	ns-1490.awsdns-58.org.
seamless.se.		86400	IN	NS	ns-1760.awsdns-28.co.uk.
;; Received 631 bytes from 192.36.144.107#53(a.ns.se) in 42 ms

seamless.se.		60	IN	A	52.77.52.233
seamless.se.		3600	IN	NS	ns-135.awsdns-16.com.
seamless.se.		3600	IN	NS	ns-722.awsdns-26.net.
seamless.se.		3600	IN	NS	ns-1490.awsdns-58.org.
seamless.se.		3600	IN	NS	ns-1760.awsdns-28.co.uk.
;; Received 175 bytes from 205.251.192.135#53(ns-135.awsdns-16.com) in 8 ms

Your TTLs and timings will differ, and the .se zone's exact server list changes occasionally. The shape will be identical.

Four blocks, and each ;; Received line names who answered. Read those lines first — they are the story.

Block 1 — from 127.0.0.53, your own machine. This is the hints being loaded and the root NS set confirmed. Nothing has left your network yet.

Block 2 — from 198.41.0.4, a root server. It did not answer "where is seamless.se". It referred you one label down, to .se. Ten NS records, no A record for your question, no aa. This is A2's referral in the wild.

Block 3 — from 192.36.144.107, one of the .se servers. Another referral, one label further: here are seamless.se's four nameservers. Still no address.

Block 4 — from 205.251.192.135, a Route 53 server. Finally, A 52.77.52.233. That server is authoritative, so it answers rather than referring.

Now the three insights that make this worth doing.

1. Nobody knew the answer until the last step. The root does not know where seamless.se is and never did. It knows who runs .se. Each step narrows by exactly one label. This is why the system scales: no server anywhere holds more than its own slice, and the root's job — knowing about ~1,500 TLDs — is tiny.

2. Look at the TTLs in the last two blocks: 86400 from the parent, 3600 from the child. The same NS RRset, two different TTLs, because it is published in two places by two different parties. That is not a bug, it is the structure — and C1 is about what happens when the two disagree on content rather than just TTL.

3. The DS and RRSIG records in block 2 are DNSSEC. .se is a signed zone, and the root is vouching for it cryptographically. Notice there is no DS record in block 3 for seamless.se — the chain of trust stops at .se, because seamless.se is unsigned. Module 07 makes that whole chain the subject.

🎯 Interview questions — Recursion and referrals

Q. Walk me through what happens when a resolver looks up www.example.com with an empty cache.

It starts from its root hints and asks a root server. The root does not know the answer; it returns a referral — the NS records for com, plus glue addresses. The resolver asks a com server, which refers it to example.com's nameservers. It asks one of those, which is authoritative and answers.

Each step resolves exactly one more label, and the resolver caches every referral along the way, so the next lookup for anything under com skips the root entirely.

The framing that shows understanding rather than recitation: no server in that chain except the last one knows the answer, and none of them ever did. The hierarchy is not a directory being searched — it is a chain of pointers, each maintained by a different organisation. That is precisely why the system scales to hundreds of millions of domains with a root zone of only about 1,500 entries.

Q. What is the difference between iterative and recursive resolution?

Recursive describes what the client asks for: "give me the final answer, do whatever work is needed". Iterative describes what the resolver does: a sequence of queries to different servers, each returning either a referral or an answer.

So a stub asks recursively, the recursive resolver resolves iteratively, and authoritative servers only ever answer iteratively — they refer or they answer, never both.

The concrete detail worth adding: it is not a capability difference, it is one bit. The client sets recursion desired; a server willing to do the work sets recursion available. An authoritative-only server never sets ra, which is what you observed in Module 01 C4.1 — and dig +trace is you personally performing the iterative walk instead of delegating it.


A4 · Glue records, and exactly when they are needed

The analogy. Think of directions that eat their own tail: "to find the branch office, ask at the branch office." You can never start.

Somebody has to put the branch's address on a sign out on the main road. That sign is a glue record — and note who owns the road: not you. It is put up by the level above you, which is why it is the piece people forget to update when they move.

A delegation gives you nameserver names. But to send a packet you need an address — so the resolver must now look up the nameserver's name. Sometimes that creates a circle.

The circular dependency, stated precisely. Suppose example.com is served by ns1.example.com. To resolve anything in example.com you must ask ns1.example.com. To find ns1.example.com's address you must resolve a name inside example.com — which requires asking ns1.example.com. The resolution cannot start.

Glue breaks the circle. The parent zone — com — publishes the address of ns1.example.com alongside the delegation, in the ADDITIONAL section. It is not authoritative for that data and says so by not setting aa; it carries it purely so resolution can begin.

And the rule falls straight out of the mechanism: glue is required only when the nameserver's name is inside the zone being delegated. If the nameservers live in a different zone, no circle exists and no glue is needed.

🧪 Exercise A4.1 — Two real delegations, one needing glue and one not
bash
# Case 1: .se is served by nameservers that are INSIDE .se
dig +short se NS | sort

# Case 2: seamless.se is served by nameservers that are NOT inside seamless.se
dig +short seamless.se NS | sort
Expected result — click to reveal
plain text
$ dig +short se NS | sort
a.ns.se.
b.ns.se.
c.ns.se.
f.ns.se.
g.ns.se.
i.ns.se.
m.ns.se.
x.ns.se.
y.ns.se.
z.ns.se.

$ dig +short seamless.se NS | sort
ns-135.awsdns-16.com.
ns-1490.awsdns-58.org.
ns-1760.awsdns-28.co.uk.
ns-722.awsdns-26.net.

Case 1 — every one of .se's nameservers ends in .se. They are in-bailiwick: inside the very zone they serve. The circle exists, so the root zone must carry glue for all ten of them. Without it, nobody could ever resolve anything in Sweden.

Case 2 — none of seamless.se's nameservers are inside seamless.se. They are in awsdns-16.com, awsdns-26.net, awsdns-58.org and awsdns-28.co.uk — four different zones, all resolvable independently. No circle, therefore no glue needed. The .se zone publishes only the four names.

This is a deliberate design choice by AWS and it is worth understanding why. Out-of-zone nameservers mean:

  • No glue to go stale. Glue lives in the parent and is updated through your registrar. It is a notoriously neglected record, and stale glue is a classic hard-to-diagnose outage
  • Addresses can change freely. Route 53 can renumber a nameserver without touching a single TLD zone anywhere
  • Four different TLDs.com, .net, .org, .co.uk — so no single TLD outage removes all four

The practical consequence for you. If you ever run your own nameservers named inside your own domain, you must keep the glue at your registrar correct, and you must update it before you renumber the server, never after. If your nameservers are your provider's, glue is not your problem — and knowing which of those two worlds you are in is a two-second check with the command above.

🎯 Interview questions — Glue

Q. What is a glue record and when is one required?

An address record for a nameserver, published by the parent zone alongside the delegation, so that resolution can begin.

It is required exactly when the nameserver's name is inside the zone being delegatedns1.example.com serving example.com. Without glue, finding that nameserver's address requires querying the zone it serves, which is circular.

If the nameservers are in a different zone — as with any managed provider, where they are typically under the provider's own domain — there is no circle and no glue is needed.

Where this earns real credit: glue lives in the parent, so it is changed through the registrar, not in your own zone file. That makes it the record people forget. Stale glue produces a delegation where some resolvers reach you and others do not, depending on whether they had cached the correct address — an intermittent, resolver-dependent failure that is miserable to diagnose. The fix in the design is what AWS does: put your nameservers outside the zone and the problem cannot occur.


A5 · What a modern resolver actually sends

The analogy. Think of the guard at the gate. He needs to know which building you are going to. He does not need to know who you are meeting, or why.

Older resolvers told everyone the whole story at every step. Modern ones say only as much as each person needs to point them onwards — which is more polite, and occasionally exposes a badly-behaved receptionist who was never asked a simple question before.

The walk in A3 has a privacy problem that went unnoticed for thirty years: at every step, the resolver sent the full name.

When resolving api.internal.seamless.se, the classic algorithm tells a root server operator, and then the .se registry, the entire name — even though neither needs to know more than the next label in order to refer you.

QNAME minimisation fixes it by asking the smallest useful question at each step. Instead of sending the full name to the root, the resolver asks only about se. To the .se servers it asks only about seamless.se. Only the final, authoritative server ever sees the complete name — and it is the only one entitled to.

It is now the default in BIND, Unbound, Knot Resolver and the major public resolvers.

The operational side effect you will actually meet: it changes what a broken or non-compliant authoritative server does. Some old servers answer NXDOMAIN for an intermediate name that has no records of its own but does have children — which is wrong, and used to be invisible because nobody ever queried those intermediate names. With QNAME minimisation they are queried on every lookup, and such a server breaks resolution for everything beneath it. If a name suddenly stops resolving on modern resolvers but still works on old ones, this is a strong suspect.

🧪 Exercise A5.1 — Query an intermediate name that has no records of its own
bash
# admin-vfo.seamless.se exists. Does the parent label chain behave?
dig +noall +comments seamless.se A
dig +noall +comments admin-vfo.seamless.se A

# now an intermediate name in a deeper hierarchy - it should be NODATA,
# NOT NXDOMAIN, because names exist beneath it
dig +noall +comments _tcp.jabber.org A
dig +noall +comments +answer _xmpp-client._tcp.jabber.org SRV
Expected result — click to reveal
plain text
$ dig +noall +comments _tcp.jabber.org A
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 18822
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1

$ dig +noall +comments +answer _xmpp-client._tcp.jabber.org SRV
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 51002
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

_xmpp-client._tcp.jabber.org. 60 IN	SRV	30 30 5222 scarlet.jabber.org.

_tcp.jabber.org returns NOERROR with zero answers — NODATA, not NXDOMAIN. That is the correct behaviour and it is what QNAME minimisation depends on.

Why the distinction is load-bearing. _tcp.jabber.org has no records of its own; it exists only as a container for _xmpp-client._tcp.jabber.org beneath it. The correct answer for a name that has children but no records is NODATA — "this node exists, nothing here". A server that answers NXDOMAIN is claiming the name does not exist at all, which by definition means nothing below it exists either.

With QNAME minimisation, a resolver asks about _tcp.jabber.org on its way down. Against a correct server it gets NODATA and continues. Against a broken one it gets NXDOMAIN, concludes the whole branch is absent, and everything below stops resolving.

The signature of this bug in production: a name works from an old resolver and fails from a new one, with no change on your side. The cause is usually an appliance or an old load-balancer-embedded DNS server sitting in front of a real zone.


Part B · The cache

B1 · What gets cached, and for how long

The analogy. Think of learning your way to a new part of town. The first trip you stop and ask three times. The second trip you ask once. After that you just drive.

And here is the part that matters: once you know how to get to the city centre, every shop in the centre becomes easy — you only need the last few streets. That is why a first-ever lookup of a brand-new domain is usually fast, not slow.

The walk in A3 took four round trips. If every lookup cost four round trips the internet would be unusable — so the resolver caches every record it learns at every step, not just the final answer.

Cached from the walkTypical TTLWhat it saves next time
Root NS set6 daysThe root step, for every lookup, forever
se NS set + glue2 daysThe root and TLD steps for every .se name
seamless.se NS set1–2 daysEverything except the final query, for every name in the zone
The A record itself60 secondsThe final query
This is why a busy resolver is almost never slow, even for names it has never seen. The infrastructure records have enormous TTLs and are shared across every domain beneath them. Once a resolver has looked up any .se name, every other .se name skips two of the four steps. A first-ever lookup of a brand-new domain typically costs one round trip, not four.

And it explains the shape of a cold-start incident. Restart a resolver and it does not get uniformly slower — it gets dramatically slower for a few seconds while it re-learns the infrastructure, then returns to normal. If you see that pattern, you are looking at a cache that was emptied.

🧪 Exercise B1.1 — Measure a cache miss and a cache hit
bash
# pick a name you have not looked up recently
dig example.org +noall +stats | grep -E 'Query time|SERVER'

# immediately again
dig example.org +noall +stats | grep 'Query time'
Expected result — click to reveal
plain text
;; Query time: 16 msec
;; SERVER: 8.8.8.8#53(8.8.8.8) (UDP)

;; Query time: 4 msec

16 ms then 4 ms. The second query never left the resolver.

Read the ratio, not the absolute numbers. On a public resolver with a warm shared cache the miss is cheap because the .org infrastructure was already cached — only the last step was real work. Against a resolver with a genuinely cold cache the first query would be 100–300 ms as it walks all four steps.

The diagnostic value of Query time. Single-digit milliseconds means a cache hit. Tens to hundreds means real work happened. If a name that should be cached is consistently expensive, either its TTL is tiny or something is evicting it — and that is worth knowing before you start blaming the network.


B2 · Negative caching, properly

The analogy. Think of remembering that a shop has closed down. If you did not remember, you would walk there every single time and be disappointed every single time.

And counter-intuitively, remembering the "no" saves more effort than remembering the "yes" — because the popular shop gets asked about once and everyone benefits, while the closed one gets asked about again and again by people who have not heard.

Module 02 B4 established that a "no" is cached, and that its lifetime is min(SOA MINIMUM, SOA TTL). Here is why it matters more than the positive cache.

Negative answers are the most valuable thing a cache holds. Typo'd names, probes for services that do not exist, malware looking for command-and-control domains, and every misconfigured client on the network generate a constant stream of queries for names that are not there. Without negative caching, every one of those would trigger a full four-step walk to prove the same absence over and over.

And that inverts the usual intuition about which cache entry is expensive. A popular name's A record is cheap to re-fetch, because everyone wants it and one fetch serves everybody. A nonexistent name may be queried by exactly one broken client, forever, and each query is a full walk. Negative caching is what keeps that from being a denial of service against the authoritative servers.

🧪 Exercise B2.1 — Watch a "no" age
bash
dig nx-probe-1.seamless.se +noall +authority | head -1
sleep 5
dig nx-probe-1.seamless.se +noall +authority | head -1
Expected result — click to reveal
plain text
seamless.se.		900	IN	SOA	ns-1490.awsdns-58.org. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400

seamless.se.		895	IN	SOA	ns-1490.awsdns-58.org. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 86400

900 → 895 across a five-second sleep. The non-existence of nx-probe-1.seamless.se is now a cached object with a countdown, exactly like a positive answer.

Note which number is counting down. It is the TTL of the SOA record in the AUTHORITY section, not the MINIMUM field inside it — MINIMUM is still showing its published 86400. The effective negative TTL is min(86400, 900) = 900 seconds, and the countdown you are watching confirms it. This is the arithmetic from Module 02 B4, demonstrated rather than asserted.

The operational rule, one more time because it costs people hours: do not query a name before you create it. The NXDOMAIN you cache is your own. Deployment scripts that poll "does the record exist yet?" in a loop are actively making the wait longer on every resolver they touch.


B3 · Resolvers do not obey your TTL

The analogy. Think of printing a best-before date on food you give away. You can print any date you like. You cannot make somebody else's fridge obey it. Some people throw it out early, plenty keep it far longer.

So a short TTL is a request, never a guarantee — which is why a failover plan that depends on everyone honouring it is not really a plan.

You publish a TTL. Resolvers treat it as advice.

Resolver controlEffect on your published TTL
max-cache-ttlCaps it. A published TTL of 7 days may be held for only 1 day
min-cache-ttlRaises it. A published TTL of 30 seconds may be held for 300 — this is the one that hurts
max-ncache-ttlCaps how long a negative answer is kept, typically at 3 hours
Serve-staleKeeps serving an expired record when the authoritative servers are unreachable — B4
The consequence you must plan around: a low TTL is a request, not a guarantee. You set 30 seconds for a fast failover; a resolver with min-cache-ttl 300 holds it for five minutes anyway. Application-level caches are worse — a JVM with the default security setting caches DNS results for the lifetime of the process, and no TTL you publish has any effect on it whatsoever.

So never design a failover mechanism whose correctness depends on clients honouring a short TTL. Design it so that a client using a stale address still reaches something — a load balancer, an anycast address, a health-checked endpoint. DNS should move traffic eventually; something else must handle it immediately.


B4 · Serve-stale

The analogy. Think of using last week's printed bus timetable when the operator's website is down. Slightly old information beats standing there with nothing. Nine times out of ten it is still correct.

The catch is the tenth time: it also hides the fact that something changed. During an outage that is a kindness. During a migration it means a broken cutover looks like a working one.

If a record's TTL expires and the authoritative servers cannot be reached, a resolver has two options: return SERVFAIL, or return the expired record it still has.

Serve-stale chooses the expired record, and the reasoning is sound. An address that was correct ten minutes ago is far more likely to work than no address at all. It converts "your authoritative servers are down, so your entire domain vanishes from the internet" into "your domain keeps working from cache while you fix it". It is on by default in BIND 9.16+ and in most large public resolvers.

The consequence to hold in your head: after your authoritative servers fail, your domain does not disappear — it freezes. Whatever was cached keeps being served. That is usually what you want during an outage, and it is emphatically not what you want during a migration, because a resolver that cannot reach your new nameservers will happily keep serving the old answers rather than telling you something is wrong.

Which produces a genuinely confusing failure mode: a migration that appears to work for most users and silently keeps sending a minority to the old infrastructure, with no error anywhere. If you are debugging that, dig +trace bypasses every cache and shows you the truth.


B5 · "DNS propagation" — what is actually happening

Official docs: RFC 2308 · RFC 1034 §4.3.4
The analogy. Think of changing your phone number. Nobody posts an announcement. There is no central update. Every person who once wrote your old number down keeps using it until they happen to ask you again.

So some friends reach you immediately, some keep ringing the old number for weeks, and both are behaving perfectly normally. That is the whole of "DNS propagation" — not a wave spreading outwards, but a lot of separate people each checking at their own moment.

Nothing propagates. There is no push, no replication to third parties, no distribution step. The phrase describes an absence of a mechanism, not a mechanism.

Here is the whole of it: you change a record on your authoritative servers. That change is live immediately for anyone who asks them. Every resolver in the world that already holds the old value keeps serving it until its own copy expires — independently, on its own clock, starting from whenever it happened to fetch.

So the honest answer to "how long will it take" is: up to the TTL that was in force on the record before you changed it, per resolver, plus whatever your provider takes to distribute the change across its own authoritative servers, plus any resolver-side TTL floor, plus any application cache.

Diagram source
flowchart LR
    CH["you change the record<br>at 12:00:00"] --> AUTH["authoritative servers<br>NEW value, immediately"]
    AUTH --> R1["resolver A<br>fetched at 11:59:50<br>TTL 300<br>serves OLD until 12:04:50"]
    AUTH --> R2["resolver B<br>fetched at 11:57:00<br>TTL 300<br>serves OLD until 12:02:00"]
    AUTH --> R3["resolver C<br>cold cache<br>serves NEW at once"]
    AUTH --> R4["resolver D<br>min-cache-ttl 3600<br>serves OLD for an hour"]
    R1 --> U["users see<br>OLD or NEW<br>depending only on<br>WHICH resolver<br>they happen to use"]
    R2 --> U
    R3 --> U
    R4 --> U
    style AUTH fill:#22c55e,color:#fff
    style R4 fill:#ef4444,color:#fff
    style U fill:#f59e0b,color:#fff
🧪 Exercise B5.1 — Catch two resolvers holding the same record at different ages
bash
for r in 1.1.1.1 8.8.8.8 9.9.9.9; do
  printf '%-10s ' "$r"
  dig @$r seamless.se A +noall +answer | head -1
done
Expected result — click to reveal
plain text
1.1.1.1    seamless.se.		41	IN	A	52.77.52.233
8.8.8.8    seamless.se.		12	IN	A	52.77.52.233
9.9.9.9    seamless.se.		58	IN	A	52.77.52.233

Same value, three different remaining lifetimes. Cloudflare fetched about 19 seconds ago, Google about 48, Quad9 about 2. Nothing is wrong with any of them.

Now picture the same three columns during a change. For the next 60 seconds one of them would show the new address and two the old, and a user's experience would be decided entirely by which resolver their ISP happens to run. That is "propagation" in full: not a wave spreading outward, but a set of independent timers expiring at different moments.

Which is also the practical verification technique. After any change, poll several public resolvers plus one authoritative server. The authoritative server tells you the change landed; the resolvers tell you how far the old answer still reaches. When all of them agree, you are done — and not before.

🎯 Interview questions — Caching

Q. How long does a DNS change take to take effect?

Up to the TTL that was in force before the change, independently per resolver — because nothing is pushed anywhere. Each cache holds its own copy on its own clock and re-fetches only when that copy expires.

So it is not one duration, it is a spread: some users see the change instantly because their resolver had nothing cached, and others see the old value for the full TTL.

The three things that make it take longer than the TTL suggests, and naming them is what marks out experience. Resolvers apply their own floors — min-cache-ttl can hold your 30-second record for five minutes. Serve-stale keeps expired records alive if your authoritative servers become unreachable. And application-level caches ignore DNS TTLs entirely; a JVM with default settings caches a lookup for the life of the process.

Q. Why is negative caching important?

Because queries for names that do not exist are constant and unbounded — typos, decommissioned services, malware beaconing, misconfigured clients. Without a cached "no", every one of them triggers a full walk from the root to prove the same absence again.

The negative TTL comes from the zone's SOA, specifically the lesser of the MINIMUM field and the SOA record's own TTL.

The inversion worth pointing out: intuitively you would think the popular records matter most in a cache, but a popular record is cheap — one fetch serves everyone. A nonexistent name might be queried by a single broken client forever, and each query is a full walk. Negative caching is what stops that being a denial-of-service against the authoritative servers.

Q. What is serve-stale and when would it hurt you?

It lets a resolver keep answering with an expired record when the authoritative servers are unreachable, on the basis that a slightly old address beats no address. RFC 8767, on by default in BIND 9.16+ and most public resolvers.

It is the right default for an outage: your domain freezes rather than vanishing.

Where it hurts is a migration. A resolver that cannot reach your new nameservers will keep serving the old answers instead of failing, so a broken cutover looks like a working one for most users while quietly sending a minority to the old infrastructure — with no error anywhere to alert you. dig +trace is the tool there, because it bypasses every cache and shows the truth from the source.


Part C · When delegation goes wrong

C1 · Parent NS versus child NS

The analogy. Think of the sign on the main road versus the sign on the building itself. Visitors follow the road sign — they have never seen your building sign and would not know where to look for it.

Repaint the building sign and forget the road sign, and everyone keeps arriving at the old place. That is why updating your zone file changes nothing until the registrar updates the delegation.

A zone's NS records exist in two places: in the parent zone as the delegation, and in the child zone at its own apex. You saw both in A3 — 86400 from .se, 3600 from Route 53.

The parent's set is the one that decides. A resolver reaches your zone by following the parent's delegation. It has never seen your apex NS records at that point and does not need them. Your zone's own NS records are documentation, plus what a resolver refreshes from once it is already talking to you.

Which is why the failure is asymmetric. Add a nameserver to your zone and forget the registrar, and resolvers never use it — the delegation does not mention it. Remove a nameserver from your zone but leave it in the delegation, and resolvers keep sending it queries it can no longer answer. The second is worse, and it has a name: C2.

🧪 Exercise C1.1 — Compare the parent's view with the child's
bash
# what the CHILD says about itself
dig +short seamless.se NS | sort

# what a .se TLD server says - the delegation.
# +norecurse stops the resolver from helpfully answering from the child.
dig @$(dig +short se NS | head -1) seamless.se NS +norecurse +noall +authority
Expected result — click to reveal
plain text
$ dig +short seamless.se NS | sort
ns-135.awsdns-16.com.
ns-1490.awsdns-58.org.
ns-1760.awsdns-28.co.uk.
ns-722.awsdns-26.net.

$ dig @a.ns.se seamless.se NS +norecurse +noall +authority
seamless.se.		86400	IN	NS	ns-135.awsdns-16.com.
seamless.se.		86400	IN	NS	ns-722.awsdns-26.net.
seamless.se.		86400	IN	NS	ns-1490.awsdns-58.org.
seamless.se.		86400	IN	NS	ns-1760.awsdns-28.co.uk.

Identical sets. That is what healthy looks like, and it is a two-command check worth running on any domain you inherit.

Note the TTL difference — 3600 from the child, 86400 from the parent — and that it is not a problem. The two copies are published by two different parties with different update cadences. Only a content mismatch matters.

Why +norecurse is essential here. Without it your resolver will helpfully go and fetch the child's answer, and you will compare the child against itself and see agreement that is not there. This is the single most common mistake when checking a delegation, and it makes a broken delegation look fine.

When they disagree, the symptom is intermittent and resolver-dependent: resolvers that happened to cache the child's set behave one way, resolvers that only ever saw the parent's behave another. Users report a problem that you cannot reproduce, which is the worst kind.


C2 · Lame delegation

The analogy. Think of a branch office still listed in the directory two years after it closed. Callers are spread across four numbers. One in four reaches the dead branch, is told "we don't handle that here", and gives up.

The other three get through perfectly. So the complaint rate is low, it never happens twice to the same person, and nobody believes there is a problem — which is exactly why this failure is so hard to pin down.

A lame delegation is a nameserver listed in a delegation that does not actually serve the zone. It is reachable — it answers — but it disclaims authority, usually with REFUSED or a non-authoritative answer.

Why lame delegations are so unpleasant to diagnose: they cause intermittent failure, not outage. A resolver picks among the delegated nameservers, often by measured latency but with a random element. With three good servers and one lame one, roughly a quarter of cold lookups hit the lame server. Some resolvers then retry another; some return SERVFAIL.

The result is a domain that works, mostly. A few per cent of requests fail. It never fails for you when you test it, and it never fails twice in a row for the same user. Teams spend days on the application before anyone checks DNS.

The usual causes are all mundane: a nameserver decommissioned in the zone but never removed at the registrar; a secondary whose zone transfer has been failing silently for months so its copy expired; or a provider migration where the old provider's servers were left in the delegation.

🧪 Exercise C2.1 — Test every delegated nameserver individually
bash
DOMAIN=seamless.se
for ns in $(dig +short $DOMAIN NS); do
  printf '%-28s ' "$ns"
  out=$(dig @"$ns" "$DOMAIN" SOA +norecurse +time=3 +tries=1 2>/dev/null)
  st=$(echo "$out" | grep -o 'status: [A-Z]*' | head -1 | cut -d' ' -f2)
  aa=$(echo "$out" | grep -o 'flags:[^;]*' | grep -c 'aa')
  if   [ -z "$st" ];        then echo "NO RESPONSE  <-- unreachable"
  elif [ "$aa" -eq 1 ];     then echo "$st  aa=yes   OK"
  else                           echo "$st  aa=NO    <-- LAME"; fi
done
Expected result — click to reveal
plain text
ns-1490.awsdns-58.org.       NOERROR  aa=yes   OK
ns-722.awsdns-26.net.        NOERROR  aa=yes   OK
ns-135.awsdns-16.com.        NOERROR  aa=yes   OK
ns-1760.awsdns-28.co.uk.     NOERROR  aa=yes   OK

All four authoritative. That is a healthy delegation and it takes about two seconds to prove.

The two things this script tests, and why both are needed. Reachability is the easy half. The half people skip is aa — a server can answer perfectly while disclaiming authority, and that is exactly what a lame server does. Testing only "did it respond?" passes a lame delegation.

What a lame server looks like:

plain text
ns-old.previous-provider.net.  REFUSED   aa=NO    <-- LAME

The nameserver is alive and well; it simply does not host this zone any more. It was never removed from the delegation.

Add this to your post-migration checklist. Every DNS provider migration should end with this loop returning aa=yes on every line, and the count of lines matching what you expect. "The website loads" is not a test of that.


C3 · Forwarding versus recursion

The analogy. Think of the difference between looking something up yourself and always asking one well-informed friend. Asking the friend is quicker — right up until the day they are unreachable, and then you have nothing at all, even though the library was open the whole time.

That is exactly what a forward-only resolver is: fast, convenient, and a single point of failure you did not realise you had chosen.

A forwarding resolver does not walk the tree. It passes the whole question to another recursive resolver and relays the answer back.

ModeBehaviour
Full recursionWalks root → TLD → authoritative itself. Independent, but every cold lookup costs several round trips
Forward-firstTries the forwarder; falls back to walking the tree itself if the forwarder does not answer
Forward-onlyUses the forwarder exclusively. If it is unreachable, resolution fails — no fallback
Conditional forwardingForwards only certain zones — corp.internal to the internal servers, everything else recursed normally. The standard hybrid-network pattern
Forward-only is a single point of failure that people configure without noticing. It is the default shape of a small office resolver pointed at the ISP, and of many container and VM defaults. When the forwarder has a bad day, your DNS is down — even though the root, the TLDs and your own authoritative servers are all perfectly healthy.

The tell in an incident: dig against your local resolver fails, dig +trace succeeds. That combination means the data is fine and your resolution path is broken, and forwarding is the first thing to check.

Conditional forwarding is the version worth knowing well, because it is how nearly every corporate and hybrid-cloud network resolves internal names while still reaching the public internet — and it is the mechanism underneath the Kubernetes and split-horizon setups in Module 08.

🎯 Interview questions — Delegation problems

Q. What is a lame delegation and how do you detect one?

A nameserver listed in a zone's delegation that does not actually serve the zone — it responds, but disclaims authority, typically REFUSED or an answer without the aa flag.

I detect it by querying each delegated nameserver directly for the zone's SOA with +norecurse, and checking two things: that it answers at all, and that aa is set. Testing only reachability misses it entirely.

Why it deserves attention out of proportion to how simple it is: it produces partial failure. Resolvers spread queries across the delegated set, so one lame server out of four means a few per cent of cold lookups fail, never reproducibly, never twice for the same person. Teams burn days on the application layer first. It is nearly always a decommissioned server left in the delegation, or a secondary whose transfers have been failing silently.

Q. Your zone's apex NS records and the parent's delegation disagree. Which wins?

The parent's — that is the set a resolver follows to reach the zone in the first place, and at that moment it has never seen the child's records.

The child's apex NS set is what a resolver may refresh from once it is already in conversation with your servers, so the two sets can both be in play in different caches at the same time.

Which is exactly why the mismatch is so nasty: the failure is resolver-dependent and intermittent. And it explains a specific real trap — adding a nameserver to your zone file does nothing until the delegation is updated at the registrar, which is a different system, a different login, and often a different team.


Part D · Field recipes

D1 · Where is this wrong answer coming from?

The analogy. Think of tracing a rumour back to its source. You ask the person who told you, then their source, then the original.

The point where the story changes is where the problem is — and until you find it, arguing about whether the rumour is true gets you nowhere.

Someone reports a stale or wrong answer. There are exactly three places it can come from, and three commands separate them.

Diagram source
flowchart TD
    S["a wrong or stale answer"] --> T1["1. dig +trace name<br>bypasses ALL caches"]
    T1 -->|"trace shows the WRONG value"| ZONE["the ZONE is wrong<br>your change never landed<br>-> fix the record"]
    T1 -->|"trace shows the RIGHT value"| T2["2. dig @auth-ns name<br>confirm at the source"]
    T2 -->|"authoritative disagrees<br>with one another"| SYNC["SECONDARIES OUT OF SYNC<br>-> check serial + transfers"]
    T2 -->|"all authoritative agree,<br>value is right"| T3["3. dig @1.1.1.1 / @8.8.8.8<br>+ the user's own resolver"]
    T3 -->|"some resolvers still old"| CACHE["CACHE<br>-> wait out the TTL<br>nothing to fix"]
    T3 -->|"all resolvers right,<br>user still broken"| CLIENT["CLIENT SIDE<br>-> /etc/hosts, app cache,<br>VPN resolver, split horizon"]
    style ZONE fill:#ef4444,color:#fff
    style SYNC fill:#ef4444,color:#fff
    style CACHE fill:#f59e0b,color:#fff
    style CLIENT fill:#8b5cf6,color:#fff
🧪 Exercise D1.1 — Run the three-command triage on a real name
bash
NAME=seamless.se

echo "=== 1. ground truth, no caches ==="
dig +trace $NAME 2>/dev/null | tail -6

echo "=== 2. every authoritative server, do they agree? ==="
for ns in $(dig +short $NAME NS); do
  printf '%-28s %s\n' "$ns" "$(dig @$ns $NAME A +short +norecurse | head -1)"
done

echo "=== 3. public resolvers, how far has it reached? ==="
for r in 1.1.1.1 8.8.8.8 9.9.9.9; do
  printf '%-10s %s\n' "$r" "$(dig @$r $NAME A +noall +answer | head -1)"
done
Expected result — click to reveal
plain text
=== 1. ground truth, no caches ===
seamless.se.		60	IN	A	52.77.52.233
;; Received 175 bytes from 205.251.192.135#53(ns-135.awsdns-16.com) in 8 ms

=== 2. every authoritative server, do they agree? ===
ns-1490.awsdns-58.org.       52.77.52.233
ns-722.awsdns-26.net.        52.77.52.233
ns-135.awsdns-16.com.        52.77.52.233
ns-1760.awsdns-28.co.uk.     52.77.52.233
=== 3. public resolvers, how far has it reached? ===
1.1.1.1    seamless.se.		41	IN	A	52.77.52.233
8.8.8.8    seamless.se.		12	IN	A	52.77.52.233
9.9.9.9    seamless.se.		58	IN	A	52.77.52.233

All three sections agree, so this domain is healthy. The value of the recipe is what it looks like when they don't, and each disagreement points somewhere completely different:

Section 2 disagrees with itself → your secondaries are out of sync. The serial was not incremented, or a zone transfer is failing. Nothing to do with caching, and waiting will not help. Module 05.

Section 1 and 2 are right, section 3 has old values → pure caching. Nothing is broken and there is nothing to fix. Read the TTLs to tell people exactly how much longer, rather than guessing.

Everything above is right and the user is still broken → the fault is on their side of the resolver: /etc/hosts, an application cache, a VPN pushing a different resolver, or split-horizon DNS giving corporate clients a different answer. That is Module 06's territory, and you have now proved DNS itself is fine before spending anyone's time on it.

The reason to run all three in order rather than picking one. Each one eliminates a layer. After three commands you can say which team owns the problem, with evidence — and being able to hand over a narrowed problem is worth more than a guess about the answer.


D2 · Full delegation health check

The analogy. Think of ringing every number on the emergency contact sheet, not just the first one. The first one always works — that is why it is first, and why nobody ever finds the problem.

The dead number is always the one nobody has called in two years.

bash
#!/usr/bin/env bash
# delegation-check.sh <domain>
set -uo pipefail
D="${1:?usage: delegation-check.sh <domain>}"
PARENT="${D#*.}"

echo "== 1. PARENT's delegation (the set that actually matters) =="
PNS=$(dig @"$(dig +short "$PARENT" NS | head -1)" "$D" NS \
        +norecurse +noall +authority +time=3 | awk '$4=="NS"{print $5}' | sort)
echo "${PNS:-  <none - is this domain delegated at all?>}"

echo "== 2. CHILD's own apex NS set =="
CNS=$(dig +short "$D" NS | sort)
echo "$CNS"

echo "== 3. do they match? =="
if [ "$PNS" = "$CNS" ]; then echo "  MATCH"
else echo "  MISMATCH  <-- intermittent, resolver-dependent failures"
     diff <(echo "$PNS") <(echo "$CNS") | sed 's/^/    /'; fi

echo "== 4. is every delegated server actually authoritative? =="
for ns in $CNS; do
  printf '  %-30s ' "$ns"
  out=$(dig @"$ns" "$D" SOA +norecurse +time=3 +tries=1 2>/dev/null)
  st=$(echo "$out" | grep -o 'status: [A-Z]*' | head -1 | cut -d' ' -f2)
  if   [ -z "$st" ]; then echo "NO RESPONSE   <-- unreachable"
  elif echo "$out" | grep -q 'flags:.* aa'; then echo "$st  aa=yes  OK"
  else echo "$st  aa=NO   <-- LAME DELEGATION"; fi
done

echo "== 5. do all of them serve the same serial? =="
for ns in $CNS; do
  printf '  %-30s serial %s\n' "$ns" \
    "$(dig @"$ns" "$D" SOA +short +norecurse 2>/dev/null | awk '{print $3}')"
done
🧪 Exercise D2.1 — Run it on your own domain
bash
chmod +x delegation-check.sh
./delegation-check.sh seamless.se
Expected result — click to reveal
plain text
== 1. PARENT's delegation (the set that actually matters) ==
ns-135.awsdns-16.com.
ns-1490.awsdns-58.org.
ns-1760.awsdns-28.co.uk.
ns-722.awsdns-26.net.
== 2. CHILD's own apex NS set ==
ns-135.awsdns-16.com.
ns-1490.awsdns-58.org.
ns-1760.awsdns-28.co.uk.
ns-722.awsdns-26.net.
== 3. do they match? ==
  MATCH
== 4. is every delegated server actually authoritative? ==
  ns-135.awsdns-16.com.          NOERROR  aa=yes  OK
  ns-1490.awsdns-58.org.         NOERROR  aa=yes  OK
  ns-1760.awsdns-28.co.uk.       NOERROR  aa=yes  OK
  ns-722.awsdns-26.net.          NOERROR  aa=yes  OK
== 5. do all of them serve the same serial? ==
  ns-135.awsdns-16.com.          serial 1
  ns-1490.awsdns-58.org.         serial 1
  ns-1760.awsdns-28.co.uk.       serial 1
  ns-722.awsdns-26.net.          serial 1

Five checks, and each catches a different real failure:

  1. An empty section 1 means the domain is not delegated at all — either it does not exist or the registrar entry is missing. That is the whole problem, and everything else is noise
  2. A section 3 mismatch is the intermittent, resolver-dependent failure from C1
  3. aa=NO in section 4 is a lame delegation — the few-per-cent failure that never reproduces
  4. NO RESPONSE is a firewall or a dead host
  5. Differing serials in section 5 means your secondaries are out of sync and users are getting different answers depending on which server they reached

Serial 1 on every line here is Route 53's constant serial, from Module 02 B4 — it replicates internally rather than by AXFR. On a BIND primary/secondary setup you would expect a date-based serial, and any difference between servers would be the finding.

Run this after every DNS provider migration and every registrar change. "The website loads" tests one resolver's cache, not the delegation.


D3 · The post-change verification playbook

The analogy. Think of moving house. You tell the post office in advance, not on the day. You check that letters actually start arriving at the new place. And you do not hand back the keys to the old house the same week — because some people will keep writing to the old address for a while, and there is nothing you can do to stop them.
WhenDoWhy
T-48hRead the current TTL. Lower it. Then wait the OLD TTL.Caches holding the old record never see the new low TTL until their copy expires. Lowering it at cutover time achieves nothing
T-1hConfirm the low TTL is visible on several public resolversProves the old TTL has drained everywhere you can see
T-0Make the change
T+1mdig @each-authoritative-ns — all agree?Proves the change landed and secondaries are in sync. If this fails, nothing else matters
T+2mdig +trace — does the walk from the root find the new value?Bypasses every cache, including serve-stale, which can hide a broken cutover
T+5mPoll 1.1.1.1, 8.8.8.8, 9.9.9.9 and the office resolver until all show the new valueThis is the only real measure of "propagation" — read the TTLs to say how much longer
T+1dRaise the TTL back upLow TTLs are query volume and a hard dependency on your authoritative servers being reachable
AlwaysKeep the old endpoint alive until every cache has drainedServe-stale and app-level caches mean some clients keep using the old address well past the TTL. Decommissioning on schedule is how you turn a clean migration into an incident
The two steps people skip, and both are the difference between a clean cutover and a postmortem. Waiting the old TTL after lowering it — skip that and the low TTL applies to nobody who mattered. And keeping the old endpoint alive — every migration has a tail of clients on stale answers, and the tail is longer than the TTL because of serve-stale and application caches.

Part E · Putting it together

E1 · How this all fits — the complete picture

Diagram source
flowchart TD
    STUB["stub resolver<br>asks once, waits"] --> REC
    REC["RECURSIVE RESOLVER"] --> C{"in cache<br>and not expired?"}
    C -->|"yes"| HIT["answer from cache<br>TTL counted down<br>no aa flag<br>single-digit ms"]
    C -->|"no"| W["walk the tree"]
    W --> R["ROOT<br>from root hints<br>-> referral to TLD<br>+ glue if in-bailiwick"]
    R --> T["TLD<br>-> referral to the zone"]
    T --> A["AUTHORITATIVE<br>-> the answer, aa set"]
    A --> STORE["cache EVERY step:<br>root NS 6d · TLD NS 2d<br>zone NS 1-2d · record its own TTL<br>negative answers too"]
    STORE --> HIT
    A -.->|"unreachable +<br>TTL expired"| STALE["serve-stale<br>expired record<br>rather than SERVFAIL"]
    style HIT fill:#22c55e,color:#fff
    style STORE fill:#f59e0b,color:#fff
    style STALE fill:#ef4444,color:#fff

Four ideas, and everything in this module follows from them.

  1. Each step narrows by exactly one label. No server holds more than its own slice — which is why the root zone has ~1,500 entries and the system serves hundreds of millions of domains.
  2. Everything learned on the way is cached, not just the answer. Infrastructure records have huge TTLs and are shared across every domain beneath them, so a first-ever lookup usually costs one round trip, not four.
  3. The parent's delegation is what a resolver follows. Your own apex NS records are not how anyone finds you.
  4. Nothing propagates. Independent caches expire on independent clocks, and resolvers may ignore your TTL in both directions.

E2 · Production practice

HabitWhy
Use dig +trace as the first command when an answer is disputedIt bypasses every cache including serve-stale, so it separates "the data is wrong" from "a cache is stale" in one step
Always pass +norecurse when querying a specific serverWithout it your resolver helpfully fetches the child's answer and you compare a set against itself
Check the parent's delegation, not just your zone's NS recordsThe parent's set is the one resolvers follow. Adding a nameserver to your zone file changes nothing until the registrar is updated
Test aa on every delegated nameserver, not just reachabilityA lame server answers happily while disclaiming authority — reachability tests pass it
Prefer nameservers outside the zone they serveNo glue means no stale glue at the registrar, and no circular dependency to get wrong
Lower TTLs and wait the OLD TTL before a changeCaches holding the old record never see the new low value until their own copy expires
Never build a failover whose correctness needs clients to honour a short TTLmin-cache-ttl, serve-stale and JVM-style app caches all ignore you. DNS moves traffic eventually; something else must do it immediately
Keep the old endpoint alive well past the TTL after a migrationThe tail of clients on stale answers is longer than the TTL, and decommissioning on schedule is how migrations become incidents
Avoid forward-only resolvers for anything that mattersIt makes your DNS depend entirely on one upstream, with no fallback to the tree
Never poll for a name before you publish itYou cache your own NXDOMAIN and lengthen the wait on every resolver you touch

E3 · Capstone exercise

No new commands — everything here is from Parts A to D. Do it without scrolling back.

Brief. For a domain you are authorised to work on, produce a delegation and cache report answering all seven:

  1. Trace the full path from the root and name who answered at each step and what type of response it was.
  2. Say which steps in that path would be skipped on a second lookup of a different name in the same zone, and why.
  3. State whether this zone requires glue records, and justify it from the NS records rather than by convention.
  4. Prove the parent's delegation and the child's apex NS set agree — using a method that cannot accidentally compare the child against itself.
  5. Prove no delegated nameserver is lame, testing the thing that reachability checks miss.
  6. Measure how far a hypothetical change would have already reached, across three public resolvers, and state the maximum remaining wait.
  7. Give the exact ordered procedure to repoint this zone's apex, including the two steps people skip.
Model answer — attempt it first, then click

1. The path. dig +trace seamless.se, reading the ;; Received lines:

  • 127.0.0.53 — local, root hints loaded, root NS set confirmed. Not a network step
  • 198.41.0.4 (a root server) — referral to .se. NOERROR, ANSWER: 0, no aa, NS records in AUTHORITY
  • 192.36.144.107 (a.ns.se) — referral to seamless.se
  • 205.251.192.135 (Route 53) — answer, aa set

2. What is skipped next time. Steps 1–3. The root NS set (6 days), the .se NS set (2 days) and seamless.se's NS set (1–2 days) are all cached. A different name in the same zone costs one query — straight to Route 53. A different .se domain entirely still skips the root and TLD steps. This is why cold-cache latency is a startup phenomenon, not a steady state.

3. Glue. Not required. seamless.se's nameservers are ns-135.awsdns-16.com, ns-722.awsdns-26.net, ns-1490.awsdns-58.org and ns-1760.awsdns-28.co.uknone of them is inside seamless.se, so there is no circular dependency to break. Glue would be required only if a nameserver were named ns1.seamless.se. Contrast .se itself, whose servers are all *.ns.se — in-bailiwick, so the root zone must carry glue for them.

4. Parent versus child.

bash
dig +short seamless.se NS | sort                                    # child
dig @a.ns.se seamless.se NS +norecurse +noall +authority | \
    awk '$4=="NS"{print $5}' | sort                                 # parent

+norecurse is the part that matters. Without it the resolver fetches the child's answer for you and you compare the child against itself — agreement that proves nothing. Both sets return the same four names: match.

5. Lameness. Query each delegated server for the SOA with +norecurse and check aa, not just that it responded:

bash
for ns in $(dig +short seamless.se NS); do
  dig @$ns seamless.se SOA +norecurse +noall +comments | grep -o 'flags:[^;]*'
done

All four return flags: qr aa rd — authoritative. A lame server would answer REFUSED, or answer without aa; a reachability test passes both, which is exactly why aa is the check.

6. Reach. dig @1.1.1.1 / @8.8.8.8 / @9.9.9.9 and read the remaining TTLs — for example 41, 12 and 58 against a published 60. Maximum remaining wait is therefore under a minute for these three. Since the published TTL is 60, no resolver should hold an old value for more than 60 seconds after a change — subject to any resolver-side min-cache-ttl floor and to application caches, neither of which I can see from outside.

7. The repointing procedure.

plain text
T-48h  read current TTL; lower it to 60; THEN WAIT THE OLD TTL      <-- skipped step 1
T-1h   confirm the low TTL is visible on 1.1.1.1 / 8.8.8.8 / 9.9.9.9
T-0    make the change
T+1m   dig @every authoritative NS - do all agree? same serial?
T+2m   dig +trace - does the walk from the root find the new value?
T+5m   poll public resolvers until all show the new value
T+1d   raise the TTL back up
       KEEP THE OLD ENDPOINT ALIVE well past the TTL                <-- skipped step 2

The five things most people miss:

  1. Waiting the old TTL after lowering it. Lowering the TTL and changing the record in the same maintenance window helps nobody who was already holding the old copy — which is everybody who mattered
  2. +norecurse in requirement 4. Omit it and the check silently becomes meaningless while still printing a reassuring "match"
  3. Checking aa rather than reachability in requirement 5. Lame servers are reachable; that is what makes them lame rather than dead
  4. Justifying glue from the NS records rather than saying "managed providers don't need glue". The rule is in-bailiwick or not, and you can read it off the output in two seconds
  5. Keeping the old endpoint alive. Serve-stale and application caches extend the tail well past any TTL you published, so the endpoint must outlive the DNS change by a wide margin

E4 · Official documentation

LinkCovers
RFC 1034 §4.3.2, §4.3.3, §5.3.3The resolver algorithm, referrals, glue and caching. Part A in its original form
RFC 2308 — Negative CachingNXDOMAIN and NODATA caching, and the min(MINIMUM, SOA TTL) rule
RFC 1912 §2.8 — NS record errorsLame delegations, parent/child mismatches, glue mistakes
RFC 8767 — Serving Stale DataWhy an expired record beats SERVFAIL, and when that hides a broken migration
RFC 9156 — QNAME MinimisationWhat modern resolvers actually send, and the NXDOMAIN-vs-NODATA bug it exposes
RFC 9499 — DNS TerminologyReferral, delegation, lame delegation, in-bailiwick, forwarder — precise definitions
BIND 9 — Name Server Operations · Configuration Referencemax-cache-ttl, min-cache-ttl, max-ncache-ttl, stale-answer-enable, forwarders
BIND 9 TroubleshootingThe vendor's own version of Part D
IANA Root Servers · Root Zone DatabaseThe 13 identities and their operators; every TLD and who runs it
How to read these efficiently.

RFC 1034 §4.3.2 is the resolver algorithm in pseudocode, and it is genuinely readable. If you only read one section of one RFC in this whole track, make it that one — everything in Part A is a paraphrase of it.

RFC 1912 §2.8 is half a page and describes lame delegation better than any blog post.

Use the BIND configuration reference by searching for the directive name, not by reading it. It is a reference, not a document.

The offline route. dig +trace is itself the best documentation of Part A — run it against a domain you know and read the ;; Received lines. man dig covers +norecurse, +trace and +nssearch with no browser.


E5 · Self-assessment

1. Why does a resolver start at the root, and how does it know where the root is?

It starts at the root because that is the only place guaranteed to know something about every name — specifically, which servers run each TLD.

It knows where the root is from a root hints file shipped with the software: the names and addresses of the 13 root identities. They are hints, not configuration — the resolver uses one to contact a root server, then fetches and caches the authoritative root NS set. That is why an out-of-date hints file still works.

2. How do you tell a referral from a NODATA answer?

Both are NOERROR with ANSWER: 0 and something in the AUTHORITY section. The difference is what that section holds.

NS records → referral: "not me, ask them." SOA record → NODATA: "it is me, and there is nothing of that type here."

A referral also never has aa set, because the server is not authoritative for the name you asked about.

3. When is a glue record required, and where does it live?

Only when a nameserver's name is inside the zone being delegated — the circular case, where finding the server's address requires querying the zone it serves.

It lives in the parent zone, published alongside the delegation and returned in the ADDITIONAL section. The parent is not authoritative for it.

Practically: it is changed through the registrar, not your zone file, which is why it goes stale — and why providers deliberately name their nameservers outside your zone so the problem cannot arise.

4. A first-ever lookup of a brand-new .se domain takes 15 ms. How, when the walk has four steps?

Because three of the four steps were already cached. The root NS set and the .se NS set have TTLs of days and are shared by every domain beneath them, so any earlier .se lookup by anyone using that resolver paid for them.

Only the final query to the zone's own nameservers was real work.

This is also why a restarted resolver is briefly and dramatically slow, then normal — it is re-learning the infrastructure.

5. Which NS set does a resolver follow, the parent's or the child's?

The parent's. That is the delegation, and it is how a resolver reaches your zone at all — at that moment it has never seen your apex records.

The child's apex NS set is what a resolver may refresh from once it is already talking to your servers, so both can be live in different caches simultaneously.

Consequence: adding a nameserver to your zone file does nothing until the registrar updates the delegation.

6. What is a lame delegation and why is it so hard to spot?

A nameserver in the delegation that does not serve the zone — it responds but disclaims authority, usually REFUSED or an answer with no aa.

It is hard to spot because it causes partial failure. Resolvers spread queries across the delegated set, so one lame server in four means a small percentage of cold lookups fail, non-reproducibly.

Detection is querying each delegated server directly with +norecurse and checking aa — reachability tests pass a lame server.

7. You publish a 30-second TTL. Name three reasons a client might still use the old value an hour later.

A resolver's min-cache-ttl raising your 30 seconds to its own floor. Serve-stale, keeping an expired record alive because your authoritative servers became unreachable. And application-level caching — a JVM with default settings caches a lookup for the life of the process and never sees your TTL at all.

The design conclusion: never build a failover whose correctness depends on clients honouring a short TTL.

8. What exactly is "DNS propagation"?

A misleading name for the absence of a mechanism. Nothing is pushed. The change is live on your authoritative servers immediately; every cache holding the old value keeps serving it until its own copy expires, independently.

So the duration is "up to the previous TTL, per resolver" — plus provider distribution time, plus resolver TTL floors, plus application caches.

The way to measure it is to poll several public resolvers and read the remaining TTLs, not to consult a propagation-checking website.

9. Why must you pass +norecurse when comparing a parent's delegation with a child's NS set?

Without it, your resolver will follow the referral and fetch the child's answer on your behalf. You then compare the child's set with the child's set, see a match, and conclude the delegation is healthy when it may not be.

It is the single most common mistake in delegation checking, and it fails in the direction of false reassurance.

10. dig against your resolver fails; dig +trace succeeds. What does that tell you?

The data is fine and your resolution path is broken. +trace walks from the root itself and bypasses your resolver entirely, so a working trace proves the zone, the delegation and the authoritative servers are all healthy.

Suspects: a forward-only resolver whose upstream is down, a firewall blocking your resolver's egress, a poisoned or broken cache entry, or split-horizon DNS answering differently for your source address.

11. Why did QNAME minimisation break some domains?

Because resolvers now query intermediate names that nobody used to query. A name that has children but no records of its own must return NODATA; some non-compliant servers return NXDOMAIN instead, which claims the entire branch beneath it does not exist.

Under the old algorithm those intermediate names were never queried, so the bug was invisible. Now it breaks resolution for everything below.

The signature is a name that works from an old resolver and fails from a modern one, with no change on your side.


Next — Module 04 · The Wire Protocol: UDP, TCP & EDNS(0).

You have read dozens of dig headers by now — flags, RCODEs, section counts, and an OPT PSEUDOSECTION that has appeared in every single response without ever being explained. Module 04 takes the message apart byte by byte: the 512-byte limit and where it came from, EDNS(0) and buffer sizes, the TC bit and TCP fallback, why fragmentation is a security problem, and why "DNS uses UDP" is only half an answer.

📚 Sources for the interview questions

All record data — root and .se NS sets and their addresses, seamless.se delegation, cache timings, negative-TTL countdowns and the _tcp.jabber.org NODATA case — was captured live on 17 August 2026 with dig 9.18. The +trace transcript in A3.1 is assembled from those verified NS sets and reflects dig 9.18's output format; run it yourself and the shape will match, though TTLs and timings will differ.

Specifications verified directly: RFC 1034, RFC 1035, RFC 1912, RFC 2308, RFC 8767, RFC 9156, RFC 9499, plus the BIND 9 ARM and IANA root server list.

Question selection cross-referenced against publicly published 2026 DNS and networking interview question sets:

Answers were rewritten and deepened rather than reproduced. The published sets stop at "the resolver asks the root, then the TLD, then the authoritative server"; the operational half — lame delegations, parent/child mismatch, +norecurse, serve-stale hiding a broken migration, TTL floors — is what actually gets probed in the room.

Spotted a mistake or want something added? Send me a note.