The happy path is short: ask a root server, get sent to a top-level domain, get sent to the domain's own servers, get the answer. networks.jelia.nyc walks that path, and this chapter assumes you have seen it. Here is everything the happy path hides: who is actually doing the walking, how a resolver starts from nothing, what a referral really contains, which records it refuses to believe, how aliases send it back to the top, what each server learns about you, and how it picks a server, gives up on one, and knows when to stop.
Common mix-up: "my DNS server" usually means a recursive resolver, and the servers that hold your domain's records are something else, authoritative servers. A resolver does not own any names; it goes and asks. An authoritative server does not go and ask anyone; it answers only from the zones it holds. Many outages are a fix applied to the wrong one of the two.
When a program on your laptop wants the address of a name, it does not walk the DNS tree itself. It calls a stub resolver, a small piece of the operating system that knows one thing: the address of a server to ask. The stub sends one question and waits for one finished answer. It cannot follow a referral and does not try.
The question often goes first to a forwarder: your home router, a corporate DNS box, or a local caching daemon. A forwarder has a cache, but when it misses it does not walk the tree either. It passes the question to another resolver upstream and relays whatever comes back.
Somewhere at the end of that line is a recursive resolver (also called a full-service or iterative resolver): your provider's, a public one, or one you run. This is the program that does the walking. It sends questions to authoritative servers, each of which holds the records for some set of zones and answers only from them. An authoritative server never goes and asks anyone else on your behalf.
Two bits in the DNS header carry the arrangement. The asker sets RD, Recursion Desired, to say "please do the whole job". A server that is willing to do that sets RA, Recursion Available, in its reply. When the recursive resolver turns to the authoritative servers it normally sends RD = 0, because it is going to follow the referrals itself, and authoritative replies come back with AA, Authoritative Answer, set when the server holds the zone. Chapter 2 shows where those bits sit in the twelve-byte header.
| Role | What it does | Sends | Typical example |
|---|---|---|---|
| Stub resolver | Asks one server one question and waits for a finished answer | RD = 1 | The resolver library in your operating system |
| Forwarder | Caches, and on a miss hands the whole question to another resolver | RD = 1 upstream | A home router, a small office DNS appliance |
| Recursive resolver | Walks the tree: follows referrals, chases aliases, caches what it learns | RD = 0 to authorities; RA = 1 to its clients | Your provider's resolver, a public resolver, Unbound, BIND 9 as a resolver |
| Authoritative server | Answers only from zones it holds; refers you down, never fetches | AA = 1 in answers from its zones | The servers named in a domain's NS records |
RFC 9499, the current DNS terminology document, defines a stub resolver as one that cannot perform all resolution itself and depends on a recursive resolver, and a full-service resolver as one that can. It also notes that "recursive" is used loosely: strictly, the resolver is iterative in how it talks to authoritative servers (one question, one referral, next question), and recursive only in what it offers its own clients.
One program can play several roles. BIND 9 can be configured as an authoritative server, a recursive resolver, a forwarder, or all three at once, and a large part of the hardening advice for it amounts to not doing that: keeping recursion open only to your own clients, and keeping authoritative service separate. Unbound, by contrast, is built only as a validating recursive resolver.
A freshly started resolver has an empty cache. To ask anything, it needs to know where the root of the tree is, and the DNS cannot tell it that, because the DNS is the thing it is trying to use. So every resolver ships with a root hints file: the names and addresses of the root servers, thirteen identities from a.root-servers.net to m.root-servers.net, each with an IPv4 and an IPv6 address. IANA publishes the file.
The hints file is a starting point, not the truth. Root server addresses do change, rarely, and a resolver may run for years on an old copy. So before its first real question, a resolver sends a priming query (RFC 8109): it picks any address from the hints, asks for the NS records of the root zone, ".", with RD = 0, and replaces what the hints said with what the root itself says, including the addresses that come back in the additional section. The hints only need to be good enough to reach one live root server once.
Priming is cheap and happens again whenever the cached root list expires. In the simulated namespace used throughout this chapter, the "root" is two servers named under .test, and the priming exchange looks like this:
; simulated priming exchange: resolver → a.root-servers.test (198.51.100.1), RD=0 ;; QUESTION . IN NS ;; ANSWER (flags: qr aa; the root is authoritative for itself) . 518400 IN NS a.root-servers.test. . 518400 IN NS b.root-servers.test. ;; ADDITIONAL a.root-servers.test. 518400 IN A 198.51.100.1 b.root-servers.test. 518400 IN A 198.51.100.2
RFC 8109 sets out the reasoning. The hints file is configuration, copied into software packages and container images, and it ages. The priming response comes from the root zone itself, so it is current, and if the resolver validates DNSSEC it can check the signature on the root's NS set (though not on the addresses, which in the real root zone are glue for names under root-servers.net). The RFC also asks resolvers to send priming queries over both IPv4 and IPv6 where they can, and to expect the response to be large enough that EDNS and a bigger buffer are needed to receive it whole.
Priming is a one-off cost at start-up and then at each expiry of the root NS set. It is not the reason root servers are busy: almost all root traffic is queries for names that do not exist, which caching of negative answers (Chapter 4) is meant to absorb. Chapter 9 covers how the thirteen identities are served by well over a thousand machines.
Ask a root server for www.shop.test and it cannot answer: it does not hold shop.test. What it holds is the delegation for .test, so it replies with a referral. A referral is a response with an empty answer section, AA = 0, and the delegation in the authority section: the NS records naming the servers for the next zone down.
A name is not enough to send a packet to. If the root said only "ask ns1.nic.test" the resolver would have to find the address of ns1.nic.test, which lives under .test, which is the zone it was trying to reach. So the parent also sends the addresses of those servers in the additional section. Those address records are called glue. Glue is not authoritative data in the parent; it is a copy kept there so that the delegation can be followed at all.
; simulated referral: a.root-servers.test answers "www.shop.test A?" ;; flags: qr (no aa: this is not an answer) ANSWER: 0, AUTHORITY: 2, ADDITIONAL: 2 ;; AUTHORITY (the delegation: who serves the next zone down) test. 172800 IN NS ns1.nic.test. test. 172800 IN NS ns2.nic.test. ;; ADDITIONAL (glue: their addresses, so the referral can be followed) ns1.nic.test. 172800 IN A 198.51.100.11 ns2.nic.test. 172800 IN A 198.51.100.12
Whether glue is needed depends on where a name server's name sits relative to the zone being delegated. RFC 9499 uses bailiwick for this: a name is in bailiwick of a zone if it is at or below that zone's apex. There are three cases worth knowing by heart.
| Name server's name | Example for shop.test | Glue? | Cost on a cold cache |
|---|---|---|---|
| Inside the child | ns1.shop.test | Required. Without it, the delegation cannot be followed | None extra, if the glue is there |
| Elsewhere in the parent | ns1.hosting.test | Optional ("sibling glue"); the parent may include it | None with glue; a lookup through hosting.test without |
| In an unrelated zone | ns1.dns-host.test serving cdn.example | Impossible: the parent has no authority over that name | A whole side lookup, from the closest known servers for that other zone |
The third row is the common one in practice: most domains are served by a DNS host whose name servers live in the host's own domain. That costs the first visitor an extra lookup, which is then cached and shared by everyone using that host. It also creates a dependency you can lose track of: your domain now resolves only if the host's domain does. Instrument 2 lets you build each case and see the extra queries, and the case where the dependencies chase each other in a circle.
RFC 9499 names the cases precisely. A name server name below the delegated zone is in-domain; one elsewhere in the parent zone, under a different delegation, is a sibling domain name server. Glue for in-domain names is mandatory. For sibling names, some servers include the addresses and some do not, which is why the same delegation can be followed in one round trip on one resolver and need a detour on another.
A referral should always point down: to a zone at or below the one the server was asked about. A server that answers a question about www.shop.test with "go ask the root" (an upward referral) is misconfigured, and old resolvers that followed such referrals could loop. RFC 1034's algorithm already says to check that the delegation is "closer" to the answer than what the resolver already has, and to discard it otherwise. The simulated resolver below does the same check.
This is a real, small recursive resolver running in your browser against a simulated namespace: a two-server root, the top-level domains .test and .example, and four zones below them. Nothing leaves your computer; all names and addresses are reserved examples. The namespace has deliberate rough edges: www.shop.test is an alias into a different zone, cdn.example is served by name servers under .test (so no glue is possible), one of shop.test's listed servers is lame, and one of the dns-host.test servers never replies. Pick a name, then press Step to send one query at a time, or Run to watch the whole walk. Switch on QNAME minimisation and the panel on the right shows exactly what each server learned. Choose a warm cache to keep everything earlier lookups taught the resolver.
Simulated. Round-trip times are fixed per server; a server that does not reply costs a simulated 800 ms timeout. The simulation's own safety limits are 30 queries, 8 alias hops and 4 nested side lookups per request. Real resolvers have similar limits with their own values, usually configurable.
A DNS response can carry records in three sections, and a server can put anything it likes in any of them. A careful resolver therefore treats every record as a claim and asks two questions before it caches it. First: does this server have any authority over that name? Second: how reliable is this kind of record, from this section, compared with what the cache already holds?
The first question is the bailiwick check. When the resolver asked a .test server, only records for names under .test are within that server's bailiwick. If a server for cdn.example tacks on an extra address for www.shop.test, the resolver ignores it, however helpful it looks. In Instrument 1, one of the cdn.example servers does exactly that, and the step log shows the record being thrown away.
The second question is ranking. RFC 2181 §5.4.1 lists, from most to least trustworthy: data from the resolver's own zone files, data from zone transfers, the authoritative answer section, the authority section of an authoritative answer, glue, non-authoritative answers, and last of all additional-section data. Higher-ranked data replaces lower-ranked data in the cache, never the other way round. Glue is good enough to send a packet to, but the RFC is explicit that the least trustworthy groups, additional data among them, should never be handed back to a client as an answer.
| Record arrives as | A careful resolver | Why |
|---|---|---|
| Answer, AA set, in bailiwick | Caches it; can answer clients with it | The server holds the zone and is speaking for its own data |
| NS in a referral, pointing down | Caches it as the delegation to follow | The parent is the right place to learn who serves the child |
| Glue for an in-bailiwick name | Uses it to reach the servers; does not answer clients with it | Low rank: a copy kept by the parent, not the child's own data |
| Any record for a name outside the server's zone | Ignores it | No authority over that name at all |
| A referral pointing up or sideways | Ignores it, treats the server as broken | Following it cannot get closer to the answer, and can loop |
These rules look fussy until you remember that a resolver's cache is shared by everyone who uses it, so a single believed lie is served to all of them. Chapter 13 is about what happens when an attacker tries to get one in, and why the bailiwick check is the first line of defense.
Here you are the owner of shop.test, delegated from .test. Choose where each of your two name servers is named, and whether .test sends its address as glue with the referral. Then the same resolver as above, with an empty cache, looks up www.shop.test. It tells you whether that worked directly, worked after a detour to find a name server's address, or could not work at all, and why. The referral box shows exactly what .test would send.
Simulated. ns1.hosting.test and ns2.hosting.test are delegated with glue; provider.example likewise from .example. loop.example's only name server is ns1.shop.test, which is how the circular preset closes its loop.
A CNAME record says "this name is an alias; the real, canonical name is that one". When a resolver asks for the address of www.shop.test and gets back www.shop.test CNAME shop.cdn.example, it has not finished. It has a new question, the address of shop.cdn.example, and it starts that question over from the closest servers it knows for the new name. If the alias points into a zone it has never visited, that means going back to the root, or at least to the top-level domain.
When an alias and its target are in the same zone, the authoritative server saves the trip: it includes the CNAME and then the target's records in the same answer. That is what cdn.example does in the simulation, answering with shop.cdn.example CNAME edge-7.cdn.example and the address of edge-7 together. When the target is in another zone, the server can only give the CNAME. Even if it added more, the resolver should not trust the extra records, by the bailiwick rule above; RFC 2181 notes that in an aliased answer, only the alias record itself is necessarily authoritative.
Chains are legal but each link can cost a full walk, and a loop of aliases (a points to b, b points to a) would never end. RFC 1034 says resolvers should follow chains and detect loops, and RFC 1536 lists alias loops among common implementation errors. In practice resolvers cap the number of hops per request and return SERVFAIL beyond it. The cap differs between implementations, so treat any chain longer than two or three links as a risk worth removing.
Content delivery networks usually ask customers to point a name at them with a CNAME, because it hands control of the final address to the CDN's own DNS, which can answer differently for different resolvers (Chapter 9). The price is the extra walk on a cold cache and a second set of TTLs to reason about. The extra walk is mostly invisible, because the CDN's zones are hot in every large resolver's cache.
A CNAME cannot sit at a zone apex alongside the SOA and NS records, and a name with a CNAME cannot have any other data. Chapter 5 covers that rule and the record types designed to work around it.
The original algorithm sends the full question to every server on the path. The root learns that someone wants the address of www.shop.test, so does .test, so does shop.test. Only the last of them needed to know. The root can only ever say "ask .test", whatever the rest of the name is.
QNAME minimisation, standardized in RFC 9156 (2021), fixes this. When the resolver asks a server that is not known to be authoritative for the full name, it sends the name cut to one label more than the zone the server is known to serve, with a query type chosen independently of the real one (A is the usual choice, because it is the least likely to upset old software). The root is asked about "test", .test is asked about "shop.test", and only shop.test's own servers see "www.shop.test". The privacy gain is real and costs nothing on a typical name.
It does cost something on deep names. A resolver does not know where the zone cuts are until it finds them, so on a name like api.v2.eu.shop.test, where all those labels live inside the one zone shop.test, a minimising resolver probes eu.shop.test, then v2.eu.shop.test, each time getting an empty "no data here" answer, before it sends the full name. RFC 9156 requires a limit on that, and recommends at most 10 minimising queries per request, adding more than one label per step once past the first few. Try api.v2.eu.shop.test in Instrument 1 with minimisation off and on, and count.
| Server | Sees, classic | Sees, minimised | Could it answer more? |
|---|---|---|---|
| Root | www.shop.test A | test A | No: it can only refer to .test |
| .test | www.shop.test A | shop.test A | No: it can only refer to shop.test |
| shop.test | www.shop.test A | www.shop.test A | Yes: this is where the answer lives |
There is a second, quieter benefit. If .test answers "nope.test does not exist" (NXDOMAIN), then nothing below nope.test can exist either. RFC 8020 makes that explicit: an NXDOMAIN for a name means the whole subtree beneath it is empty, and a resolver may answer any later question under that name from its cache without asking. A minimising resolver hits these cuts higher up the tree, so it learns them sooner. Look up x.y.nope.test in Instrument 1 with a warm cache, then look up nope.shop.test: the first is stopped at .test, the second at shop.test.
The first version, RFC 7816 (2016), suggested sending the NS query type for the probes. Some authoritative servers and middleboxes answered NS queries for empty non-terminals wrongly, with NXDOMAIN instead of an empty NOERROR, and a minimising resolver that believed them would decide that real names did not exist. RFC 9156 relaxed the query type (A or AAAA is fine), and resolvers added a fallback: if a minimised probe gets an NXDOMAIN or an error that looks wrong, retry with the full name. Unbound documents a "relaxed" mode that does just this, and a strict one that does not.
Minimisation does not hide anything from the server that answers, and it does not hide anything from your resolver, which still sees every question you ask. For that, see encrypted transport in Chapter 12.
Every zone is supposed to have at least two name servers, and big ones have many. Which one should the resolver ask? RFC 1034 already describes the idea: keep a list of the servers for the zone, ordered by what you know about them, mainly how fast they have answered before. Modern resolvers keep a smoothed round-trip time (SRTT) per server address: a running average that moves part of the way toward each new measurement, so one slow reply nudges it rather than resetting it.
Always picking the lowest SRTT has a trap: a server that was slow once never gets asked again, even after it recovers. So resolvers keep exploring. Some slowly lower the remembered time of servers they are not using, so that eventually one looks attractive enough to retry. Others choose at random among all servers within some band of the fastest. Both converge on fast servers while keeping the rest measured. The details, and the constants, differ by implementation; the BIND 9 and Unbound documentation describe their own.
When a server does not reply at all, the resolver waits for a timeout, records the failure (in the SRTT, as a very slow answer), and sends the same query to the next server. A server that replies but refuses, or answers without authority for a zone it is listed as serving, is a lame server: the delegation names it, but it does not serve the zone. Resolvers remember lameness for a while and skip such servers. If every server for a zone fails, the client gets SERVFAIL, and RFC 9520 (2023) now requires resolvers to cache that failure briefly too, so that a broken zone is not hammered by retries from every client at once.
Simulated. Each query goes to one server; a lost packet costs an 800 ms timeout and a retry to another server. Smoothing moves the SRTT 30% of the way to each new sample; under "decay", unused servers' SRTT shrinks by 2% per query; under "band", the pick is random among servers within 100 ms of the fastest. These constants belong to this simulation, not to any particular resolver.
The last line of defense is a budget. A resolver walking a broken or hostile delegation could otherwise spend unbounded work on a single client question: chasing aliases, starting side lookups for name server addresses that need their own side lookups, retrying servers that never answer. RFC 1034's algorithm opens with the instruction to "bound the amount of work (packets sent, parallel processes started) so that a request can't get into an infinite loop". Real resolvers cap queries per client request, depth of nested lookups, alias hops and the number of name servers they will try per delegation, and answer SERVFAIL when a cap is reached. Those caps are why a delegation with dozens of out-of-bailiwick name servers is not just slow but can fail outright on some resolvers.
Here is the walk Instrument 1 performs for www.shop.test with an empty cache and minimisation off. Every row is one packet out and, unless it timed out, one back. Run it yourself to see the cache fill in between.
| # | Asked | Question | Reply | Simulated ms |
|---|
Read it as a list of costs. One packet primes the root. Two take the resolver from the root down to shop.test's delegation. One is wasted on the lame server. One gets the alias. Two more take the resolver from the root down to cdn.example's delegation, one is a side lookup only because cdn.example's name servers live in another tree, one is lost to a dead server, and the last gets the answer. Nearly every one of those is cached afterwards, which is why the same question a minute later costs nothing, and why a lookup of a neighbor, such as mail.shop.test, costs one packet.