๐ Query types: recursive, iterative, forwarding
Most DNS conversations on a real network are recursive: a client asks one resolver one question and gets one answer. But "recursive" is a role the resolver plays, not a kind of packet on the wire. That distinction explains why forwarders and conditional forwarding behave the way they do.
The three things a resolver can do with your query#
When a client sends dig example.com A to a resolver, the resolver can respond in one of three ways:
- Answer from cache. Already knows. Returns immediately. (Most queries.)
- Recurse. Walks the hierarchy itself (root โ TLD โ authoritative), assembles the answer, returns it, caches it.
- Forward. Hands the question to a different resolver (an "upstream") and waits for that server to do the work.
Cases 2 and 3 are both "recursive" from the client's perspective: the client sets the RD flag (Recursion Desired = 1) and gets a single answer back. What happened upstream is the resolver's business.
Key rule: recursion implies forwarding#
If a resolver is configured to recurse, every query it can't answer from cache will leave its interface heading somewhere: either to the roots, or to a configured upstream. Recursion implies forwarding.
This is the rule of thumb to keep at hand. The corollary: if recursion is off, your "forwarders" list does nothing. An authoritative-only server with recursion disabled treats its forwarders configuration as inert.
That sounds obvious until you've spent twenty minutes debugging "the forwarder isn't working" only to discover recursion got toggled off. Common across home routers, commercial appliances, and managed DNS services alike.
Three query flavors on the wire#
Recursive query (RD=1)#
client --RD=1--> resolver --...do the work...--> resolver --answer--> clientThe client trusts the resolver to do whatever it needs to do. The client never speaks to the authoritative server directly: that is the resolver's job. Every stub resolver sets RD by default; the resolver does the walking.
dig example.com Anslookup -type=A example.comResolve-DnsName example.com -Type AIterative query (RD=0)#
client --RD=0--> resolver
returns whatever it has, with referrals if needed
client itself walks the next hopUsed between resolvers and authoritative servers. A recursive resolver iterates on the client's behalf: first asking a root server, then the TLD, then the zone's authoritative servers, until it has a final answer.
Use +trace to watch this happen; dig does the iterating manually, printing each referral step.
dig +trace example.com A. 518400 IN NS a.root-servers.net.
;; Received 811 bytes from 127.0.0.53#53(127.0.0.53) in 1 ms
com. 172800 IN NS a.gtld-servers.net.
;; Received 1176 bytes from 198.41.0.4#53(a.root-servers.net) in 12 ms
example.com. 172800 IN NS hera.ns.cloudflare.com.
example.com. 172800 IN NS elliott.ns.cloudflare.com.
;; Received 309 bytes from 192.5.6.30#53(a.gtld-servers.net) in 20 ms
example.com. 300 IN A 104.20.23.154
example.com. 300 IN A 172.66.147.243
;; Received 88 bytes from 173.245.58.162#53(hera.ns.cloudflare.com) in 9 msEach block is one referral hop, abbreviated to one line per level. The first block is dig priming itself: it asks your normal resolver (here systemd-resolved on 127.0.0.53) for the root server list, then walks everything else on its own. The Received ... from line under each block names the server that sent that block, so the com. referral arrives from a root server and the example.com. referral from a .com gTLD server. From there dig does the iterating itself (root โ TLD โ authoritative) exactly as a recursive resolver would, but printed step by step. Real output also includes RRSIG and DS records at each hop, because +trace turns on +dnssec automatically ("+dnssec is also set when +trace is set", BIND 9 dig manual). Every value here varies by run: which root and TLD instances dig happens to hit, the byte counts, and the timings. example.com is hosted on Cloudflare today, so its authoritative servers (hera / elliott.ns.cloudflare.com) and answer addresses look different from a name hosted elsewhere.
nslookuphas no+traceequivalent. PowerShell can do the lookup but not the walk:
Resolve-DnsName example.com -Type A+norec on dig sets RD=0, useful for poking an authoritative server directly (no recursion,
answer from owned zones only). The server that owns the zone returns aa (authoritative answer)
and NOERROR; a server that does not own the zone either refers or refuses. Cloudflare's
nameservers refuse: ask coleman.ns.cloudflare.com for a zone Cloudflare does not host and you
get REFUSED with no aa, plus an extended error explaining why
(EDE 20, "Not Authoritative", RFC 8914).
Demo status: the
probe.isitdns.netdemo server is being rebuilt, so these specific lookups may not answer right now. The mechanics shown here are unchanged; the transcripts below are what a healthy exchange looks like.
Here pns1.isitdns.net is the authoritative server for probe.isitdns.net, so it answers directly:
dig @pns1.isitdns.net probe.isitdns.net SOA +norec;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: ...
;; flags: qr aa; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
probe.isitdns.net. 300 IN SOA pns1.isitdns.net. hostmaster.isitdns.net. \
2026061511 7200 3600 1209600 300aa is set because pns1 owns the zone. rd is absent because the client sent RD=0, and the
server correctly leaves it unset in the response.
nslookup -norecurse -type=SOA probe.isitdns.net pns1.isitdns.netNetwork intercept caveat: on a network that intercepts port 53 (captive portals, some home routers), a
@serverquery is answered by the interceptor instead of the named server. You may see onlyrd rawithoutaa; that is your network, not the server.
Cloudflare's coleman or hera work too. An authoritative server answers RD=0 queries for the zones it owns with
aaset (RFC 1034 section 4.3.2); RD=0 is exactly what every recursive resolver sends it, sodig @coleman.ns.cloudflare.com isitdns.net SOA +norecreturns a cleanaaNOERRORjust like thepns1transcript above. Cloudflare's anti-amplification measure lives elsewhere: minimal responses toANYqueries (RFC 8482), not refusing RD=0. A self-hosted BIND, Knot, NSD, or CoreDNS instance works for the demo as well.
Forwarding#
client --RD=1--> local resolver --RD=1--> upstream resolver --answer--> ...The local resolver doesn't iterate: it just passes the buck. This is what most enterprise resolvers actually do: forward to a curated upstream (Cloudflare, Quad9, a corporate Internet-facing recursive cluster) rather than walking from the root themselves. See forward for how forward zones are configured.
Why forward instead of recurse to root?
- Cache locality. The upstream has a fatter, hotter cache.
- Egress control. Easier to monitor and filter one egress point than every resolver in the org.
- Policy/RPZ. The upstream applies threat-intel filtering you don't want to duplicate.
- DNSSEC offload. Some clients delegate validation to a trusted upstream (see dnssec for the trade-offs).
It's common to chain forwarders: client โ local resolver โ org-level resolver โ roots. Each hop honors RD=1; the last hop iterates.
Conditional forwarding#
You can forward only some queries based on the queried name. Useful in multi-DNS environments where different parent zones live on different authoritatives.
| Pattern | Example |
|---|---|
Forward *.internal.lan to an internal DNS | A subsidiary's namespace lives on another team's resolvers |
Forward *.consul to a service discovery resolver | Hashicorp Consul, a Kubernetes cluster's internal resolver |
| Forward in-addr.arpa for a partner's IP space | Reverse lookups for a connected customer |
Most recursive resolvers expose this under names like "Forward Zones," "Stub Zones," or "Conditional Forwarders." Pure authoritative services (the cloud-DNS providers) don't expose conditional forwarding because it's an upstream-feature concept. See delegations for how zone authority is partitioned across servers.
What you'll see in dig flags#
After every dig answer, the flags line tells you what just happened:
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1| Flag | Meaning | When set |
|---|---|---|
qr | This is a response, not a query | Always on answers |
rd | Recursion was desired | Almost always: client wants the resolver to do the work. Servers echo it back in the response even when they will not recurse, so rd without ra from an authoritative server is normal |
ra | Recursion is available | Resolver is willing and able to recurse: off on authoritative-only servers |
aa | Authoritative answer | The responder owns the zone (rare for typical dig output: you'll see this when you @<authoritative-server>) |
ad | Authenticated data | Set by the resolver after it validates the DNSSEC chain. Requesting +dnssec sets the DO bit (asks for RRSIGs) but does NOT guarantee ad; the resolver sets ad only when it validates. Can be stripped on the path. See dnssec. |
cd | Checking disabled | Client asked the resolver to skip DNSSEC validation |
A handy check: query a recursive resolver and confirm ra is set.
dig @1.1.1.1 example.com A;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1nslookup -type=A example.com 1.1.1.1dig shows ra in the flags line; nslookup confirms the answer resolves but cannot display flags (use dig or a DoH client for ra). Do not read anything into the missing ad here: example.com is DNSSEC-signed and 1.1.1.1 validates it, but 1.1.1.1 only sets ad when the query signals DNSSEC interest (add +dnssec and ad appears), while 8.8.8.8 sets ad on the plain query. Both behaviors are allowed (RFC 6840 section 5.8). In RFC terms, an authoritative-only server should leave ra unset to signal it will not recurse, and the big managed authoritative providers (Cloudflare, Route 53, NS1) do exactly that: query one of their nameservers directly and ra is absent. So ra=0 is easy to observe in the wild: any large provider's authoritative nameserver shows it, as does a self-hosted authoritative server (BIND, NSD, or Knot) with recursion explicitly disabled. If you query an authoritative server and see ra set anyway, suspect a port-53 interceptor on your own network answering in its place.
What "forwarders enabled but empty" looks like#
A classic misconfiguration across home routers, commercial appliances, and open-source DNS servers alike:
- Recursion is on.
- Forwarders are enabled as a feature.
- The forwarders list itself is empty.
The resolver silently iterates from root for everything. Sometimes that's what someone wanted; often not. Audit dig +trace from the resolver itself if you're not sure which path queries are actually taking.