Full lesson
Explore the full explanation, examples, and visuals at your own pace.
orders-api logs (illustrative)
Both Pods say Running, yet the app can't reach its database. Look at the error: a lookup timeout against 10.96.0.10, port 53. The connection never even reached Postgres. The failure happens earlier, while the app is trying to turn a name into an address.
$ kubectl -n shop logs deploy/orders-api --tail=3
ERROR db connect failed: dial tcp: lookup
postgres.shop.svc.cluster.local on 10.96.0.10:53:
read udp 10.244.1.7:41022->10.96.0.10:53: i/o timeout
$ kubectl -n shop get pods
NAME READY STATUS RESTARTS
orders-api-7c 1/1 Running 0
postgres-0 1/1 Running 0The healthy path: name, Service, Pod
Here's the healthy path. In namespace shop, orders-api wants Postgres. The Service holds a stable ClusterIP, 10.96.45.12 on port 5432, and kube-proxy routes it to postgres-0. But the app only knows postgres.shop.svc.cluster.local. Something has to turn that name into the IP first.
Step zero: resolve the name
Watch the order. orders-api reads resolv.conf, which points at the kube-dns Service, 10.96.0.10. That Service routes the query to a CoreDNS Pod. CoreDNS answers from Service records it watches through the API, returning 10.96.45.12. Only then does TCP on port 5432 begin. The database connection never starts without that first answer.
ndots:5 turns one lookup into many queries
With ndots:5, a name with fewer than five dots isn't treated as absolute. So api.stripe.com tries each search suffix first, and each one returns NXDOMAIN. Often that happens for both A and AAAA records. One connection becomes several queries, and that load lands on CoreDNS.
CoreDNS saturated: the Service is fine, the directions are not
The postgres Service is healthy, and postgres-0 is ready to answer. But orders-api never gets there. Its query goes to kube-dns, lands on a throttled CoreDNS, and times out.
Every connection by Service name first asks CoreDNS for directions. If that step stalls, nothing else runs.
Two failure shapes
Overloaded CoreDNS answers some queries slowly and drops others, so you see latency spikes and intermittent timeouts across unrelated apps. Unavailable is harsher: no ready endpoints, so every new lookup fails. Either way, it looks like a flaky database or downstream service, even though the real fault is DNS.
Why it surfaces late
An outage can hide at first. Pooled connections to postgres are already open, so they need no lookup. Failures show up on reconnects, rollouts, or new Pods. And resolver caching varies by runtime, so each app behaves differently.
- Open DB connections need no lookup
- Reconnects and new Pods do
- Client resolver caching varies by runtime
CoreDNS goes down for two minutes. Which orders-api requests are most likely to fail first?
CoreDNS goes down for two minutes. Which orders-api requests are most likely to fail first?
- Queries on already open database connections
- Requests needing a new database connection
- Every request immediately, regardless of connections
CoreDNS goes down for two minutes. Which orders-api requests are most likely to fail first?
Requests needing a new database connection. An open connection already knows the ClusterIP, so it needs no lookup. Failures appear when the app reconnects, scales, or restarts and must resolve the name again. That delay is why DNS outages often look intermittent rather than total.
- Queries on already open database connections
- Requests needing a new database connection
- Every request immediately, regardless of connections
Isolate one layer at a time
Start with the name. Can orders-api's hostname resolve? If not, you've found a DNS problem. If it does, bypass DNS and hit the ClusterIP directly. A failure there points to the network. If that works and the app still fails, look at the database. The first no localizes the fault.
Run the tree from a debug Pod
We start a throwaway netshoot Pod in the shop namespace, then run the tree in order. dig on the name times out. nc straight to the ClusterIP on 5432 succeeds, and pg_isready accepts connections. Outputs are illustrative, but this pattern pins the fault on DNS.
kubectl -n shop run net-debug --rm -it \
--image=nicolaka/netshoot -- bash
# 1) DNS: does the name resolve?
dig +short postgres.shop.svc.cluster.local
# ;; connection timed out; no servers could be reached
# 2) Network: skip DNS, hit the ClusterIP
nc -zv -w 3 10.96.45.12 5432
# Connection to 10.96.45.12 5432 port [tcp/postgresql] succeeded!
# 3) Database: is Postgres accepting?
kubectl -n shop exec postgres-0 -- pg_isready
# /var/run/postgresql:5432 - accepting connectionsInspect CoreDNS itself
Once DNS is the suspect, look at CoreDNS itself. Check replica count, node spread, and restarts, including OOMKilled. Confirm the kube-dns Service has ready endpoints. Then read recent logs for errors, and compare kubectl top against the configured CPU and memory limits.
# Replicas, placement, restarts
kubectl -n kube-system get pods -l k8s-app=kube-dns -o wide
kubectl -n kube-system describe pods -l k8s-app=kube-dns \
| grep -A3 'Last State' # OOMKilled?
# Does the Service have ready endpoints?
kubectl -n kube-system get endpointslices \
-l kubernetes.io/service-name=kube-dns
# Errors and load
kubectl -n kube-system logs -l k8s-app=kube-dns --tail=50
kubectl -n kube-system top pods -l k8s-app=kube-dnsWhat the evidence means
A failing ready check on 8181 means CoreDNS isn't serving. Upstream i/o timeouts or SERVFAIL point to the forward step. CPU at its limit, throttling, or OOMKills point to capacity. Metrics on 9153 can show latency.
- Readiness failing on :8181, health on :8080
- Logs: upstream i/o timeout, SERVFAIL
- CPU at limit, throttled, or OOMKilled
Mitigations: cache closer, scale out, ask less
To reduce the blast radius, put a NodeLocal cache next to orders-api so repeat lookups never leave the node. Run several CoreDNS replicas, with enough CPU to avoid throttling. And ask less: a fully qualified name with a trailing dot skips the search-path queries that ndots:5 triggers.
Name first, then network, then database
Every connection by Service name first asks CoreDNS for directions. So when orders-api can't reach postgres, don't guess. Test the name, then the ClusterIP, then the database protocol, separately. The first layer that fails tells you where to look.






