When name resolution is the problem
Half the incidents that get filed as network problems are DNS problems, and they're filed wrong because the symptom looks identical: a service that can't reach another service. The difference is that in a DNS failure, no packet was ever sent to the destination — the application gave up before it had an address to send anything to.
The resolution process article covered how a name becomes an address in theory. This one is about the four ways that goes wrong on a real Linux host, and the specific commands that tell them apart.
dig and your application don't ask the same question
This trips up experienced people, so get it out of the way first.
Both commands ran on the same host, seconds apart, and they disagree. Neither is broken.
dig is a DNS tool. It builds a DNS query and sends it straight to a nameserver over UDP port 53. That's all it does.
An application resolves names through the C library's Name Service Switch, which consults sources in the order listed in /etc/nsswitch.conf:
files means /etc/hosts. dns means an actual DNS query. The bracketed directive means that if mdns4_minimal returns "not found," resolution stops there and never reaches dns — which is one real way a name that resolves fine with dig fails for everything else on the box.
So use the tool that answers the question your application is asking:
getent hosts goes through NSS exactly as an application does — /etc/hosts first, then DNS, honouring every switch rule along the way. When dig and getent disagree, believe getent, and then find out why they differ. The answer is almost always a stale entry in /etc/hosts that somebody added during a migration two years ago:
Which resolver is even being used
On modern Ubuntu, /etc/resolv.conf rarely names a real DNS server:
127.0.0.53 is systemd-resolved, a local stub listening on loopback. Your queries go there, and it forwards them to the upstream servers it learned from DHCP or netplan. So the server in /etc/resolv.conf is not the server actually answering, and pointing dig at 127.0.0.53 tells you what the stub has cached, not what the authoritative source says.
To see the real upstreams and the per-interface configuration:
Global
Protocols: -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
resolv.conf mode: stub
Link 2 (eth0)
Current Scopes: DNS
Protocols: +DefaultRoute -LLMNR -mDNS -DNSOverTLS
Current DNS Server: 10.20.0.2
DNS Servers: 10.20.0.2 10.20.0.3
DNS Domain: internal.example.com
Now you can bypass the cache and ask the upstream directly, which is how you tell a stale local cache from a genuinely wrong record:
If the upstream returns the new address and your application still gets the old one, the stub's cache is stale:
That command is safe and takes effect immediately. It is also the one people reach for far too early — flush after you've confirmed a mismatch, not as a first move, or you'll destroy the evidence that proves the cache was the problem.
Stale answers have a source, and it's the TTL
A record was changed an hour ago. Some clients see the new address, some see the old one. Nothing is broken — you're watching a TTL expire.
3542 is the seconds remaining before this cached answer expires. Ask again in ten seconds and it will read 3532. A resolver that cached the old value will keep serving it for exactly that long, no matter what the authoritative server now says, and there is no mechanism to reach into other people's caches and correct them.
Which gives you a rule with real operational teeth:
Lower the TTL before you plan to change a record, not after. Drop it to 60 seconds a day ahead of a migration, make the change, verify, then raise it back. Changing a record that's still published with a 24-hour TTL means up to a day of split traffic, and no amount of flushing fixes it for anyone but you.
To see what the authoritative server says, skipping every cache in the path, ask it directly:
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; ANSWER SECTION:
api.example.com. 3600 IN A 203.0.113.44
The aa flag in the flags line means authoritative answer — this server owns the zone and isn't repeating something it cached. That's the ground truth, and any disagreement between it and what your host sees is a caching problem somewhere in between.
NXDOMAIN is cached too, and that surprises people
Create a DNS record, and it doesn't work — for a while — even though the record clearly exists now. What happened is that something queried the name before it existed, got NXDOMAIN, and cached that negative answer.
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 41022
;; AUTHORITY SECTION:
example.com. 900 IN SOA ns1.example-dns.net. admin.example.com. 2026080301 7200 3600 1209600 900
The status: NXDOMAIN says the name doesn't exist. The lifetime of that negative answer is the last field of the SOA record — 900 here — not the TTL you set on the record you just created. That's defined behaviour, not a quirk: negative caching uses the zone's SOA minimum field.
So "I created the record and it still doesn't resolve" has a specific answer: wait out the SOA minimum, or flush the caches you control. And it has a specific prevention: don't point clients at a name before the record exists.
Search domains, and why one lookup becomes five
That search internal.example.com line in resolv.conf isn't cosmetic. A name with no trailing dot and fewer dots than the ndots setting gets the search domains appended and tried in turn before the name is tried on its own.
This is invisible when it works and very visible when it doesn't. Inside Kubernetes, the default ndots is 5, and a pod resolving api.example.com — three dots, under the threshold — will first try api.example.com.<namespace>.svc.cluster.local, then api.example.com.svc.cluster.local, then api.example.com.cluster.local, and only then the name as written. Four queries fail before the fifth succeeds.
Every one of those adds latency, and if the cluster's DNS is under load, every one of them is a chance to time out. The symptom is external lookups that are slow or intermittently fail while in-cluster lookups are fine.
The fix is a trailing dot, which marks the name as fully qualified and skips the search list entirely:
In application configuration, the same trailing dot in a hostname (https://api.example.com./) or a pod-level dnsConfig with a lower ndots does the same job.
A worked failure
A batch job on worker-03 started failing overnight with Could not resolve host: db.internal. Nothing was deployed. Run the ladder:
Resolution genuinely fails the way the application sees it. Next, is it DNS specifically, or the whole resolver path?
The upstream nameserver isn't answering at all. That reframes the incident completely: this isn't a record problem, it's a reachability problem to the DNS server — and the tools for that are the ones from the previous article.
Traffic to the DNS server leaves via eth1 with a source address in 10.20.5.0/24. On the working hosts it leaves via eth0 from 10.20.0.0/24. Somebody added a route overnight, and the DNS server's firewall only permits queries from the original subnet.
Not one of those five commands was about DNS records. The DNS error was real; the DNS configuration was fine.
UDP makes DNS reachability tests slightly awkward
nc -zv -u reports success for a UDP port whenever nothing comes back with an ICMP port-unreachable, which for a filtered port looks the same as an open one. Treat a UDP "succeeded" as weak evidence. A dig against that server, which expects a real answer, is the stronger test — and DNS servers also listen on TCP/53, so nc -zv 10.20.0.2 53 gives you a definite TCP answer for the same host.
Practice
- Add an entry to
/etc/hostsfor a name that already resolves publicly, pointing it somewhere else. Then comparedig +short,getent hosts, andcurl -vagainst that name and explain why they differ. - Query a record with
digtwice, thirty seconds apart, and watch the TTL count down. Then query the authoritative server for the same name and explain why its TTL doesn't count down. - Find the SOA minimum for a domain you control with
dig +short SOA <domain>and state how long a mistaken NXDOMAIN would persist in caches. - On a host using
systemd-resolved, find the real upstream DNS servers without reading/etc/resolv.conf. - Write out the exact sequence of names a Kubernetes pod in namespace
paymentswould query forredisand forapi.example.com, givenndots:5. Then explain which of the two benefits from a trailing dot.
If exercise 1 produced three different behaviours, you've internalised the thing this article exists to teach: "DNS" is not one system, and the tool you test with decides which part of it you're testing.
Sources
- IETF, RFC 2308 – Negative Caching of DNS Queries (DNS NCACHE) — defines the SOA minimum as the negative-cache lifetime.
- Linux man-pages, nsswitch.conf(5)
- Linux man-pages, resolved.conf(5)
- Kubernetes, DNS for Services and Pods — documents the
ndotsdefault and poddnsConfig.