Every pod scheduled with the default DNS policy gets a /etc/resolv.conf it never asked for. The Kubernetes docs show what kubelet writes there:
nameserver 10.32.0.10
search <namespace>.svc.cluster.local svc.cluster.local cluster.local
options ndots:5Two lines of that file conspire to turn a single outbound HTTP call into a burst of DNS queries, most of which are guaranteed to fail. To see why, you have to read the search line and the ndots option together — and then count dots in the names Kubernetes promised your applications they could resolve.
What ndots actually decides
Per resolv.conf(5), ndots sets the threshold for how many dots must appear in a name before the resolver tries it as an absolute name first. The man page's own phrasing of the default behavior: with ndots:1, "if there are any dots in a name, the name will be tried first as an absolute name before any search list elements are appended to it."
So the rule is simple:
- Name has ≥ ndots dots → query it as-is first; fall back to the search list only if that fails.
- Name has < ndots dots → walk the search list first, appending each suffix in order, and only try the name as-is last.
The libc default for ndots is 1, and the value is silently capped at 15. Kubernetes overrides it to 5. That single digit changes the resolution order for almost every hostname your application touches, because very few real-world names have five dots.
The multiplication
Take a pod calling api.stripe.com — two dots, well under five. The resolver refuses to treat it as absolute and walks the search list first:
api.stripe.com.→ NXDOMAIN.svc.cluster.local api.stripe.com.svc.cluster.local→ NXDOMAINapi.stripe.com.cluster.local→ NXDOMAINapi.stripe.com→ finally, an answer
Four name attempts, three of them structurally incapable of succeeding. And that's the name count, not the query count: since glibc 2.9, the stub resolver sends the A and AAAA lookups in parallel for each name (that's what the single-request option exists to disable, per the man page). Eight queries on the wire, six guaranteed NXDOMAINs — for one curl.
On managed clouds it's often worse. The node's own search domain frequently gets appended to the pod's list. The resolv.conf in Marco Pracucci's writeup of this behavior has four search domains:
search namespace.svc.cluster.local svc.cluster.local cluster.local eu-west-1.compute.internal
options ndots:5That's five name attempts, ten queries, eight dead on arrival. Every one of those NXDOMAINs is a round trip through your cluster DNS, and every one is load CoreDNS has to absorb. The failure mode isn't just latency on one call — it's that your DNS infrastructure serves several times the query volume the application "logically" generates, with the overhead concentrated on exactly the lookups (external APIs, databases outside the cluster) where you'd least expect cluster DNS to be in the path at all.
The tail behavior is uglier than the average. resolv.conf(5) gives the resolver a default per-query timeout of 5 seconds (RES_TIMEOUT) and 2 attempts (RES_DFLRETRY). A single dropped UDP packet inside that expansion chain doesn't cost you one query — it costs you a 5-second stall in the middle of a sequence the application perceives as "resolving one hostname."
Why 5 and not 1
Given all that, ndots:5 looks like a blunder. It isn't — it's the smallest value that keeps Kubernetes' documented naming contract working. The cluster promises that relative service names resolve via the search path. Count the dots in the forms the DNS spec and docs define, written relative to the cluster zone:
| Name form | Example | Dots |
|---|---|---|
| Same-namespace service | data | 0 |
| Cross-namespace service | data.prod | 1 |
| Explicit svc form | data.prod.svc | 2 |
| Headless pod hostname | pod-0.data.prod.svc | 3 |
| SRV record | _https._tcp.data.prod.svc | 4 |
The SRV schema is fixed by the Kubernetes DNS specification as _ — so its relative form always carries four dots. For the search list to expand it into the full name, the threshold has to sit above four. Set ndots:2 cluster-wide and data.prod.svc — a form the docs explicitly support — gets tried as an absolute internet name first and leaks out of the cluster before falling back. The value 5 is the naming contract, priced in DNS queries.
The docs' own cross-namespace example shows the contract in action: a pod in namespace test queries data.prod, which only resolves because one dot is under the threshold and the svc.cluster.local suffix gets appended. Lower ndots carelessly and that lookup changes meaning.
Paying less without breaking the contract
You don't get to keep all five relative forms and have single-query external lookups. But you can choose where to pay:
Add a trailing dot. api.stripe.com. is a fully qualified, rooted name — the search list never applies, regardless of ndots. Zero config changes, works per-hostname, and it's the fix for static endpoints in app config. The catch: some libraries and TLS-verification code paths choke on the trailing dot, so it needs testing, not blanket application.
Override ndots per pod. The dnsConfig field merges options into the generated resolv.conf — the docs' example sets ndots:2. For a pod that only talks to external APIs and other clusters, ndots:1 or 2 is safe; you only lose the zero-and-few-dot relative forms that pod wasn't using. This is a per-workload decision, which is exactly why it's a pod-spec field and not a cluster flag.
Use full FQDNs for internal calls. data.prod.svc.cluster.local. (rooted) resolves in one name attempt and still hits CoreDNS. Verbose, but it makes manifests unambiguous.
Two boundary conditions worth knowing before you tune any of this. First, search-list capacity: glibc 2.25 and earlier silently capped the search list at 6 domains and 256 characters; glibc 2.26 removed the limit, and since Kubernetes v1.28 (stable) the platform itself allows up to 32 search domains and 2048 total characters — though the docs note containerd ≤ 1.5.5 and CRI-O ≤ 1.21 could leave pods Pending when the list got long. Second, none of this maps to Windows nodes: Windows treats any name containing a dot as an FQDN and skips expansion entirely, so a manifest that relies on data.prod expansion is quietly non-portable.
ndots:5 is one of those defaults that looks wrong from below (the packet capture full of NXDOMAINs) and inevitable from above (the naming spec it exists to serve). The useful move isn't to fight the default — it's to notice which of the five relative name forms each workload actually uses, and stop paying for the rest.
