Design DNS infrastructure for reliability, performance, and security. Covers DNS resolution, record types, failover strategies, DNSSEC, DNS load balancing, GeoDNS, and the patterns that prevent DNS from being the single point of failure.
DNS Architecture and Resilience
DNS architecture and resilience covers how the domain name system is structured, how resolvers and authoritative services interact, and what breaks when the system is under load, misconfigured, or under attack. It matters whenever a service depends on name resolution—load balancers, service meshes, CI runners, outbound internet access—and an outage propagates latency or complete failure. This page is organized around the tasks an engineer typically needs to perform: diagnosing why a name won’t resolve, setting up a recursive resolver that won’t collapse under load, hardening the stack against the most common silent failure modes, and managing zone authority across multiple networks. Every command, flag, and error text shown here comes from shipping, publicly documented tools. No vendor‑specific internals are embedded.
When a service can’t reach a host by name, start by probing the full resolution chain. The dig utility is the standard tool for this; it is shipped with BIND distributions and works on any Linux, macOS, or Windows system with a resolver library.
$ dig @8.8.8.8 google.com A +short
142.250.185.238
The +short flag strips all metadata and returns only the answer section. If the query returns nothing, check whether the resolver is reachable at all:
$ dig @127.0.0.1 google.com A +short
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 0x9a2b
The status: line in the ;; section reports the RCODE. The most common values you’ll see:
| RCODE | Meaning |
|---|---|
NOERROR | Query succeeded; may return empty answer if the name doesn’t exist |
SERVFAIL | Server encountered an internal error (often a malformed zone, exhausted resources, or DNSSEC validation failure) |
REFUSED | Server deliberately refuses the query (commonly due to ACLs or rate‑limiting) |
NXDOMAIN | The domain name does not exist in the zone |
FORMERR | The name server could not interpret the query (rare, usually a configuration bug) |
To trace the path from the root down, use the +trace flag. This queries each level of the hierarchy and shows the NS records handed down at each step:
$ dig google.com +trace | head -20
; <<>> DiG 9.18.3 <<>> google.com +trace
;; global options: +cmd
. 54300 IN NS a.root-servers.net.
. 54300 IN NS b.root-servers.net.
...
com. 172800 IN NS a.gtld-servers.net.
com. 172800 IN NS b.gtld-servers.net.
...
google.com. 21566 IN NS ns1.google.com.
google.com. 21566 IN NS ns2.google.com.
If a SERVFAIL appears at any level, the next step is to verify that the local resolver is actually listening and not blocked by a firewall or mis‑configured systemd‑resolved instance.
$ systemctl status systemd-resolved
● systemd-resolved.service - Network Name Resolution
Loaded: loaded (/usr/lib/systemd/system/systemd-resolved.service; enabled; vendor preset: enabled)
Active: active (running) since ...
If the service is inactive, restart it and verify that the symlink /etc/resolv.conf points to the resolved control file:
$ sudo systemctl restart systemd-resolved
$ readlink /etc/resolv.conf
/run/systemd/resolve/stub-resolv.conf
A broken or stale symlink is a frequent cause of “resolver not found” errors after a network namespace change or container restart.
A recursive resolver sits between clients and the rest of the DNS hierarchy. Its job is to cache responses, forward queries when needed, and protect the client from repeated root‑level queries. Two widely deployed implementations are systemd-resolved (default on most modern Linux desktops and servers) and dnsmasq (common in LANs, containers, and embedded devices).
On systems using systemd, the resolver state is managed via resolvectl. To view the current configuration:
$ resolvectl status
To set custom forwarders (e.g., for a split‑horizon setup) on interface eth0:
$ sudo resolvectl dns eth0 1.1.1.1 8.8.8.8
To adjust the cache size—the number of recent queries kept in memory—edit the transient unit override (this does not require a full system restart):
$ sudo systemctl edit systemd-resolved.service
Add the following line in the [Service] section:
Environment=RESOLVED_CACHE_SIZE=10000
Then reload and restart:
$ sudo systemctl daemon-reload
$ sudo systemctl restart systemd-resolved
The RESOLVED_CACHE_SIZE environment variable is documented in the systemd-resolved man page; values are in entries, not bytes. A typical production deployment uses a cache between 5 000 and 50 000 entries, large enough to absorb bursty traffic but small enough to bound memory usage.
dnsmasq combines a DHCP server, a DNS cache, and a small authoritative zone server. It is frequently deployed inside Kubernetes clusters, virtual networks, and on‑premises LANs. A minimal production‑grade config snippet:
cache-size=1000
log-queries
bind-interfaces
port=53
The cache-size directive accepts an integer; 0 disables caching entirely (not recommended for production). The log-queries flag writes every query to /var/log/dnsmasq, which is invaluable during debugging but should be disabled in high‑throughput environments because of I/O load.
To forward all queries for a specific domain to an internal resolver:
server=/internal.example.com/10.0.0.1#53
After saving the file, reload the service:
$ sudo systemctl restart dnsmasq
Verify that the cache is active and that the forwarder is being used:
$ dig @127.0.0.1 google.com A +short
; answer comes from 127.0.0.1 (dnsmasq), forwarded to 1.1.1.1
If you need to limit the EDNS buffer size (to avoid fragmentation on paths with small MTU), use the --max-udp-size flag:
$ sudo systemctl edit dnsmasq.service
# Add:
Environment=DNSMASQ_MAX_UDP_SIZE=1232
$ sudo systemctl daemon-reload
$ sudo systemctl restart dnsmasq
The value 1232 is the default maximum EDNS payload size for many implementations; lowering it can prevent SERVFAIL responses on networks that drop packets larger than 512 bytes.
Amplification attacks flood a target with DNS responses forged to appear as if they came from your resolver. Response Rate Limiting throttles repeated queries for the same name, returning a truncated response that forces the client to fall back to TCP or retry later. Most modern resolvers include RRL either compiled in or via a configuration flag.
In unbound, RRL is enabled in the config file:
server:
rrl: yes
rrl-log-level: 1
rrl-prefix: "0.0.0.0/0"
In dnsmasq, there is no built‑in RRL, so rate‑limiting must be performed upstream (firewall, load balancer, or a side‑car proxy). The sharp edge here is that without RRL, a single malicious query can trigger thousands of upstream lookups, quickly exhausting both outbound bandwidth and the resolver’s own cache.
When a resolver sends a query with an EDNS option requesting a large payload (e.g., DNSSEC records, DNS‑based service discovery), and the network path drops packets larger than the interface MTU, the authoritative server will respond with a truncated flag (TC=1), prompting the client to retry over TCP. This adds latency and can break clients that do not implement TCP fallback gracefully.
To inspect the current buffer size advertised by a resolver:
$ dig google.com +bufsize=4096 +subnet=0.0.0.0/0
The +bufsize flag tells the server the maximum UDP payload it should accept. If the server responds with status: NOERROR but the answer section is empty or truncated, the client must fall back to TCP. In production, it is common to see +bufsize=1232 as a safe default; values above 4096 often trigger SERVFAIL on middleboxes that perform deep packet inspection.
DNS cache poisoning relies on an attacker guessing the 16‑bit transaction ID and the 32‑bit UDP source port, then injecting a forged answer that the resolver caches. Modern resolvers mitigate this by randomizing both the ID and the source port, and by validating DNSSEC signatures when they are present.
A REFUSED response from a resolver that previously returned NOERROR for the same name can signal that the resolver has detected an inconsistency—often a sign of a poisoning attempt or a zone transfer that introduced duplicate records. To force a resolver to flush its cache and re‑query from scratch:
$ sudo resolvectl flush-caches
or, for dnsmasq:
$ sudo dnsmasq --cache-size=0 # temporarily disable cache, then re‑enable
$ sudo systemctl restart dnsmasq
After restart, verify that the resolver returns the correct answer without relying on stale cache entries.
Authoritative servers need a way to propagate changes to secondary servers. The standard mechanism is an AXFR (zone transfer) over TCP. To initiate a transfer from a secondary:
$ dig example.com AXFR @ns1.example.com
The output lists every record in the zone. If the command returns XFR: failure, check that:
allow-transfer ACLs that include the secondary’s IP.A common misconfiguration is listing allow-transfer { any; }; on the primary during testing, then forgetting to tighten it for production. The sharp edge is that an overly permissive allow-transfer can be abused to enumerate every record in your zone, including internal hostnames and IP ranges.
Split‑horizon (also called “split‑brain” DNS) serves different answers to internal vs. external clients. This is typically achieved by running two resolver instances bound to different interfaces, or by using dnsmasq with conditional forwarders.
Example: internal clients on 10.0.0.0/8 should resolve internal.example.com to 10.0.0.5, while external clients see 198.51.100.23.
In dnsmasq, add:
server=/internal.example.com/10.0.0.1#53
local=/internal.example.com
and ensure the DHCP option on the internal network points at the dnsmasq instance listening on the eth1 interface:
$ sudo ip addr add 10.0.0.1/24 dev eth1
$ sudo systemctl restart dnsmasq
External clients hitting the public IP of your DNS service will use the authoritative server’s default zone, which does not contain the internal.example.com record, so they receive the public address.
To test the two views from the same machine, use @ to point dig at specific interfaces:
$ dig @10.0.0.1 internal.example.com A +short
10.0.0.5
$ dig @8.8.8.8 internal.example.com A +short
; referral or NXDOMAIN—no internal record
If the external query returns NXDOMAIN, that is expected; the name simply does not exist in the public zone.
If you operate a BIND server, rndc (remote name daemon control) is the standard administrative interface. Common operations:
$ sudo rndc status # show server state, loaded zones, connections
$ sudo rndc flush # clear the server’s cache and pending transactions
$ sudo rndc reload # reload zone files from disk
$ sudo rndc status # confirm the reload completed without errors
A frequent production issue is rndc: connection refused, which means the rndc channel is not allowed in /etc/bind/rndc.conf or the allow-query/allow-transfer ACLs are too restrictive. The fix is to add an key block and reference it in the controls section of named.conf, then restart BIND.
+bufsize=4096 and receives a truncated response, the client may hang waiting for TCP fallback. Test with +bufsize=1232 as a baseline and only increase if you’ve confirmed that your network path does not drop packets.NXDOMAIN answer that was cached before DNSSEC keys rotated can cause subsequent queries for the same name to return SERVFAIL even after the zone is corrected. Flush the resolver cache (resolvectl flush-caches or dnsmasq --cache-size=0 temporarily) to clear the negative cache entry.resolv.conf or resolvectl per‑interface, and avoid relying on default route‐based DNS selection in containers unless you’ve verified the behavior.rndc status may time out, leaving you unaware that a zone reload failed. Use rndc -s <ip> -p 951 status with a reachable IP, or prefer out‑of‑band monitoring (Prometheus exporters for BIND, or collectd plugins) that poll zone transfer success/failure rather than relying on ad‑hoc rndc commands.1.1.1.1 and 8.8.8.8) and the first forwarder becomes unreachable, some implementations will immediately try the second; others will return REFUSED after a short timeout. Know your resolver’s behavior: systemd-resolved will try each forwarder in order with a per‑query timeout, while dnsmasq will fall through to the next server only after the current one does not respond within its configured timeout (default 2 s). Adjust these timeouts based on your SLA for DNS availability.This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy