Your dashboards show an upstream DNS hiccup that lasted one second. Your users report an outage that lasted five. Both are accurate, and the gap between them is the CoreDNS cache plugin doing what it was designed to do.
CoreDNS caches SERVFAIL responses for 5 seconds by default. The cache plugin keeps separate positive (success) and negative (denial) caches, and SERVFAIL goes into the denial cache alongside NXDOMAIN and NODATA. The intent is sound: RFC 2308 permits caching server-failure responses to shield an already-struggling upstream from a retry storm. The side effect is that a single SERVFAIL answer for a hot record is served to every client that asks during the 5-second window, even after the upstream has fully recovered.
This article covers the amplification mechanism, how to confirm it is what you are seeing (rather than a genuinely longer upstream outage), and when tuning the SERVFAIL cache TTL is the right call.
What this means
The sequence:
- An upstream resolver flaps for one second. A packet drop, a rate-limit kick, a brief overload.
- A client queries a popular name. The forward plugin gets no usable answer and returns SERVFAIL.
- The cache plugin stores that SERVFAIL in the denial cache with a 5-second TTL.
- The upstream recovers one second later.
- For the remaining four seconds, every client query for that record is answered from cache: SERVFAIL, instantly, without touching the upstream.
The amplification factor is the query rate for the affected name. At 1,000 QPS for a hot record, one upstream failure becomes roughly 5,000 SERVFAIL responses. Client-side retry logic does not rescue you: the system resolver (glibc) treats SERVFAIL as a final answer and does not retry, and applications with their own retry loops receive the cached SERVFAIL again. A one-second flap becomes a five-second outage for every client, and your SERVFAIL rate chart looks five times worse and lasts five times longer than the underlying fault.
flowchart TD
A[Upstream flaps for 1 second] --> B[Client query for hot name]
B --> C[Forward fails: SERVFAIL returned]
C --> D[Cache stores SERVFAIL in denial cache, TTL 5s]
D --> E[Upstream recovers at t=1s]
E --> F{Another query, t=2s}
F -->|denial cache hit| G[SERVFAIL served from cache]
F -->|t > 5s, entry expired| H[Fresh forward: NOERROR]There is a second trap during cache collapse events. After a rolling restart empties the cache, clients re-query everything at once. If the upstream stumbles under that flood, CoreDNS caches the resulting SERVFAILs for 5 seconds each, stretching a self-correcting thundering herd into something that looks like a sustained outage. The SERVFAIL cache does not cause the upstream failure, but it determines how long your users keep seeing it after the cause is gone.
Common causes
The amplification mechanism is always the same; what varies is the brief underlying failure that seeds the cached SERVFAIL.
| Cause | What it looks like | First thing to check |
|---|---|---|
| Transient upstream flap | SERVFAIL rate spikes for ~5s, then cleanly returns to zero. Upstream health checks may show one or two failures, or none if the flap was shorter than the check interval | coredns_forward_healthcheck_failures_total deltas around the event window |
| Upstream rate limiting | Recurring 5-10s SERVFAIL pulses at regular intervals, often when a batch job or deploy kicks off. The upstream drops excess queries silently | Per-upstream latency in coredns_forward_request_duration_seconds{to=...}; look for one upstream that is fine on health checks but fails real queries |
| Cache collapse after restart or reload | SERVFAILs cluster in the minutes after a rollout; cache hit ratio is near zero and climbing | Correlate the event with deployment timing and coredns_cache_entries dropping to zero |
| Slow upstream drag tipping into timeout | Latency climbs first, then SERVFAIL appears. Cached SERVFAILs persist 5s after latency recovers | coredns_forward_request_duration_seconds P99 leading the SERVFAIL spike |
| Longer denial TTL configured | Outage duration matches a custom denial TTL rather than the 5s default | Read the Corefile cache block |
If you are running a CoreDNS version old enough to still use the deprecated proxy plugin, the equivalent metric names are coredns_proxy_* instead of coredns_forward_*.
Quick checks
All read-only. Run these during or immediately after the event; the denial cache entries themselves expire in 5 seconds, so you are mostly working from counters. The commands assume the default metrics endpoint on port 9153.
# SERVFAIL responses, split by the plugin that generated them
curl -s http://localhost:9153/metrics | grep 'coredns_dns_responses_total' | grep 'SERVFAIL'
# Denial cache hits: are SERVFAIL/NXDOMAIN answers coming from cache?
curl -s http://localhost:9153/metrics | grep 'coredns_cache_hits_total'
# Did upstreams actually flap, and which one?
curl -s http://localhost:9153/metrics | grep 'coredns_forward_healthcheck_failures_total'
curl -s http://localhost:9153/metrics | grep 'coredns_forward_healthcheck_broken_total'
# What does the Corefile cache block say right now?
kubectl get cm -n kube-system coredns -o yaml # Kubernetes
# or: cat /etc/coredns/Corefile # standalone
Two things to note in the output. First, the plugin label on SERVFAIL responses tells you whether forward produced them (upstream problem) or another plugin did. Second, coredns_cache_hits_total{type="denial"} covers NXDOMAIN, NODATA, and SERVFAIL together; you cannot separate SERVFAIL cache hits from ordinary negative-cache hits by metric alone. In Kubernetes, most denial hits are search-domain NXDOMAIN noise, so treat a denial-hit spike as corroborating evidence, not proof.
How to diagnose it
Establish the true upstream outage window. Look at
coredns_forward_healthcheck_failures_totaland per-upstream latency over the incident window. If health checks barely moved but SERVFAIL spiked, the upstream fault was shorter than the visible impact.Check the SERVFAIL tail. Plot the SERVFAIL rate second by second (or as finely as your scrape interval allows). The signature of cache amplification is a sharp onset followed by a cliff-edge drop roughly 5 seconds after the upstream signal recovers. A genuine ongoing upstream failure decays messily or not at all.
Compare client-facing reports with the upstream timeline. If users report a 5-second failure but upstream health checks show 1 second of trouble, cache amplification is the bridge. If both show 5 seconds, go investigate the upstream instead. See CoreDNS returning SERVFAIL: the resolver is failing queries and what to check first.
Confirm the cache is involved. Denial cache hit rate climbing during the SERVFAIL window, while forward traffic to the affected upstream stays flat, means queries are being answered from the denial cache rather than re-forwarded.
Check for the cache-collapse variant. If the event followed a restart, reload, or rolling update, look at
coredns_cache_entriesdropping to zero and upstream request rate spiking. The SERVFAILs are secondary; the root cause is the cold-cache thundering herd.Read the Corefile. Confirm whether the
cacheblock sets a customdenialTTL or aservfaildirective. A denial TTL set far above the default makes the amplification window much longer than 5 seconds.
Metrics and signals to monitor
| Signal | Why it matters | Warning sign |
|---|---|---|
coredns_dns_responses_total{rcode="SERVFAIL"} by plugin | The user-facing pain signal; the plugin label isolates forward vs kubernetes origin | Sustained nonzero rate; spikes that outlast upstream evidence |
coredns_cache_hits_total{type="denial"} against misses | Shows how much of the SERVFAIL tail is cache-served | Denial hit rate spiking in step with SERVFAIL responses |
coredns_forward_healthcheck_failures_total{to=...} | Ground truth for the real upstream failure window | Brief increment followed by silence while SERVFAIL continues |
coredns_forward_healthcheck_broken_total | Confirms complete upstream loss vs a partial flap | Any increment during the window points away from pure cache amplification |
coredns_forward_request_duration_seconds{to=...} | Per-upstream latency; catches the slow-then-fail pattern | P99 spike preceding the SERVFAIL spike |
coredns_cache_served_stale_total | If serve_stale is configured, shows stale serves masking upstream state | Rising stale serves alongside SERVFAIL |
On alerting: SERVFAIL alone is not page-safe, and cache amplification is one reason why. Transient flaps produce SERVFAIL ratios that self-resolve within seconds. Page only on composite rules: SERVFAIL corroborated by upstream failure evidence, sustained for more than 5 minutes, and not during a cold-start window. For the distinction between SERVFAIL and benign negative answers, see CoreDNS NXDOMAIN vs SERVFAIL: why alerting on the wrong one buries real incidents.
Fixes
Tune the SERVFAIL cache TTL
Since CoreDNS v1.9.4, the cache plugin accepts a servfail DURATION directive that overrides the 5-second default. Setting servfail 0 disables SERVFAIL caching entirely. The TTL is capped at 5 minutes per RFC 2308.
. {
forward . 10.0.0.2 10.0.0.3
cache 30 {
servfail 1s
}
}
Tradeoffs, stated plainly:
servfail 0or a very low TTL: clients see upstream recovery almost immediately, and retry loops start working again. The cost is that during a genuine upstream outage, every query for a failing record re-forwards and re-fails, adding load to an upstream that is already down and latency to every client. On a hot record at high QPS, that can be the difference between an upstream that recovers and one that stays down.- Keep the 5-second default: hot records are shielded, upstreams get breathing room during real outages, but every brief flap is multiplied by five for every client.
- A middle value (1-2s): most of the retry-storm protection, most of the fast recovery. Reasonable for clusters where upstreams flap briefly but often (cloud provider DNS rate limiting is the classic case).
Before v1.9.4 there is no knob: SERVFAIL caching is hardcoded at 5 seconds, and the only way to stop it is to disable the cache plugin entirely, which is almost never worth it.
Fix the flap, not the cache
Tuning the TTL shrinks the blast radius of brief failures, but the failures themselves have a cause. If per-upstream health checks show recurring short failures on one upstream, replace or remove it. If the pattern is rate limiting (AWS VPC DNS resolver limits per ENI are a documented trigger), spread CoreDNS across nodes, add upstreams, or deploy a node-local cache. See CoreDNS per-upstream health check failures: degraded redundancy before total loss.
Prevent the cold-cache amplifier
If your SERVFAIL bursts follow rollouts, the fix is rollout hygiene, not cache tuning: stagger restarts, set maxUnavailable=1, and use PodDisruptionBudgets so the cluster never cold-starts its entire cache at once. A Corefile reload flushes the cache too, so config changes carry the same thundering-herd risk.
Be careful combining serve_stale with SERVFAIL caching
With serve_stale enabled, CoreDNS versions before v1.14.4 could prefer a cached SERVFAIL in the denial cache over a still-valid positive entry, hiding upstream recovery until the stale window expired. v1.14.4 changed the lookup to prefer the positive entry. If you run serve_stale and observe recovery delays longer than 5 seconds, check your CoreDNS version first.
Prevention
- Alert on composites, not raw SERVFAIL. Require upstream failure evidence plus duration. The 5-second amplification guarantees that brief flaps produce scary-looking spikes that resolve on their own.
- Dashboard the denial cache separately. A denial-hit spike overlapping a SERVFAIL spike is your fastest confirmation of amplification during an incident.
- Know your Corefile. Record whether
servfail, customdenialTTLs, andserve_staleare set. During an incident, the configured value is the expected tail length; if the observed tail does not match it, the cache is not your problem. - Remove flaky upstreams aggressively. One upstream that flaps for a second every few minutes generates a permanent low-grade SERVFAIL background at 5x amplification.
- Stagger anything that clears the cache. Rollouts, reloads, and config pushes are the other multiplier.
How Netdata helps
Netdata surfaces the specific correlations that make this failure mode recognizable in seconds rather than after a postmortem:
- SERVFAIL responses split by plugin and zone, so you can tell a forward-plugin failure (upstream origin) from other sources without grepping metrics by hand.
- Per-second SERVFAIL rate against upstream health check failures on the same timeline, which makes the amplification signature (SERVFAIL tail outlasting the upstream blip) directly visible.
- Denial cache hits and cache hit ratio alongside forward traffic, so you can see SERVFAILs being served from cache instead of re-forwarded.
- Per-upstream latency breakdown, catching the slow-then-fail pattern where latency leads the cached SERVFAIL wave.
- Cache entries dropping to zero correlated with pod restarts, identifying the cold-cache thundering-herd variant before you tune the wrong knob.
- Anomaly detection on response codes, flagging SERVFAIL bursts that are short enough to slip under ratio-based alert thresholds but frequent enough to indicate a flapping upstream.
Related guides
- CoreDNS returning SERVFAIL: the resolver is failing queries and what to check first
- CoreDNS NXDOMAIN vs SERVFAIL: why alerting on the wrong one buries real incidents
- CoreDNS all upstreams down: the forwarding black hole and healthcheck_broken
- CoreDNS per-upstream health check failures: degraded redundancy before total loss
- CoreDNS monitoring checklist: the signals every production resolver needs
- How CoreDNS actually works in production: the plugin chain mental model






