DNS negative caching countdown between an authoritative server and recursive resolver
Last edited on August 10, 2026

You add a DNS record, confirm that the authoritative server returns it, and still see NXDOMAIN from the resolver used by your browser, server, or monitoring check. Both observations can be correct. The resolver may have cached an earlier negative answer and will not ask the authority again until that negative cache entry expires.

The useful question is therefore not simply “does the record exist now?” It is “which DNS component answered, what kind of negative answer did it cache, and how much of that answer’s lifetime remains?” This article builds that proof with an isolated BIND 9 authority and an Unbound recursive resolver. The reproduced run cached both NXDOMAIN and NODATA for eight seconds, published the missing data at the authority, observed the resolver continue returning its negative answers, and then received the positive records after expiry.

Two Empty Results Mean Different Things

DNS clients often flatten several outcomes into “not found,” but the protocol does not. NXDOMAIN says the queried name does not exist. NODATA is the common operational label for a NOERROR response with no answer of the requested type: the name exists, but that record type does not.

Result What the authority asserted Practical cache scope
NXDOMAIN The queried name does not exist Name and class; another type at that name can be suppressed too
NODATA The name exists, but the requested type does not Name, type, and class; another type can still answer
SERVFAIL The resolver could not complete or validate the lookup A transient failure, not proof that the name or type is absent

That scope changes the repair. If api.example.com A was cached as NXDOMAIN, adding only an AAAA record does not make the cached assertion true. If api.example.com AAAA was cached as NODATA while its A record already existed, the A answer can remain available while the cached AAAA absence counts down.

Do not group SERVFAIL with either case. A broken delegation, unreachable authority, or validation failure needs a different investigation. Voxfor’s guides to CAA SERVFAIL versus an empty answer and DNSSEC chain-of-trust SERVFAIL show why response codes must be separated before changing records.

Read the Negative Clock From the SOA

RFC 2308 defines how an authoritative negative response carries the zone’s SOA record. The cache lifetime is the smaller of the SOA record’s own TTL and the SOA MINIMUM field. In compact form:

negative TTL = min(SOA RR TTL, SOA.MINIMUM)

If the SOA line has a 900-second record TTL and a 300-second MINIMUM, the negative answer can be cached for 300 seconds. If those values are reversed, it can be cached for 300 seconds again. The final field is not automatically the timer unless it is also the smaller value.

RFC 9520 updates operational requirements for negative caching. Resolver software can also apply local ceilings and eviction behavior. The BIND 9 reference documents max-ncache-ttl, while the Unbound configuration reference exposes cache-max-negative-ttl. Managed DNS platforms may impose their own SOA values; for example, Amazon Route 53 documents how its SOA TTL and minimum value determine negative caching.

A displayed TTL is a countdown, not the original policy. Ask the same resolver twice a few seconds apart. A falling SOA TTL is direct evidence that a cached negative response is aging. A constant or reset value may mean the query reached a different resolver, the record was refreshed, or an intermediate layer answered.

Establish Two DNS Roles in a Disposable Lab

This lab uses Debian’s BIND 9 and Unbound packages, listens only on loopback ports 15353 and 15354, uses reserved documentation addresses, and refuses pre-existing paths or occupied ports. Run it only on a disposable host where you are authorized to start those processes. The authoritative fixture begins without missing.cache-lab.voxfor-lab A and without node.cache-lab.voxfor-lab AAAA; its SOA record TTL is 12 seconds and its MINIMUM is eight, so the expected negative TTL is eight.

set -Eeuo pipefail
umask 077

LAB=/var/cache/bind/voxfor-dns-ncache-119-lab
UNBOUND_LAB=/var/lib/unbound/voxfor-dns-ncache-119-lab
AUTH_PORT=15353
RECURSOR_PORT=15354
ZONE=cache-lab.voxfor-lab
NAMED_PID=
UNBOUND_PID=

[[ "$LAB" == /var/cache/bind/voxfor-dns-ncache-119-lab ]]
[[ "$UNBOUND_LAB" == /var/lib/unbound/voxfor-dns-ncache-119-lab ]]
[[ ! -e "$LAB" && ! -e "$UNBOUND_LAB" ]]
for tool in named named-checkconf named-checkzone unbound unbound-checkconf dig; do
  command -v "$tool" >/dev/null
done
! ss -H -lntu | awk '{print $5}' | grep -Eq ":(${AUTH_PORT}|${RECURSOR_PORT})$"
install -d -m 0700 "$LAB" "$UNBOUND_LAB"

cleanup() {
  set +e
  if [[ -n "${UNBOUND_PID:-}" ]] && kill -0 "$UNBOUND_PID" 2>/dev/null; then
    kill "$UNBOUND_PID"
    wait "$UNBOUND_PID" 2>/dev/null
  fi
  if [[ -n "${NAMED_PID:-}" ]] && kill -0 "$NAMED_PID" 2>/dev/null; then
    kill "$NAMED_PID"
    wait "$NAMED_PID" 2>/dev/null
  fi
  [[ "$LAB" == /var/cache/bind/voxfor-dns-ncache-119-lab ]] && rm -rf -- "$LAB"
  [[ "$UNBOUND_LAB" == /var/lib/unbound/voxfor-dns-ncache-119-lab ]] && rm -rf -- "$UNBOUND_LAB"
}
trap cleanup EXIT

printf 'NAMED=%s\nUNBOUND=%s\nDIG=%s\n' \
  "$(named -v)" "$(unbound -V | sed -n '1p')" "$(dig -v)"

The authority is deliberately non-recursive. Version 1 contains one existing A record. Version 2 increments the serial and adds both previously absent answers. Keeping both versions makes the publication event reproducible instead of asking the reader to edit a live zone during the test.

cat >"$LAB/named.conf" <<EOF
options {
  directory "$LAB";
  listen-on port $AUTH_PORT { 127.0.0.1; };
  listen-on-v6 { none; };
  recursion no;
  allow-query { 127.0.0.0/8; };
  pid-file "$LAB/named.pid";
  session-keyfile "$LAB/session.key";
};
controls { };
zone "$ZONE" { type primary; file "$LAB/zone.live"; };
EOF

cat >"$LAB/zone.v1" <<'EOF'
$ORIGIN cache-lab.voxfor-lab.
$TTL 30
@ 12 IN SOA ns.cache-lab.voxfor-lab. hostmaster.cache-lab.voxfor-lab. (
  2026081001 60 30 3600 8
)
@    IN NS ns.cache-lab.voxfor-lab.
ns   IN A  192.0.2.53
node IN A  192.0.2.10
EOF

cat >"$LAB/zone.v2" <<'EOF'
$ORIGIN cache-lab.voxfor-lab.
$TTL 30
@ 12 IN SOA ns.cache-lab.voxfor-lab. hostmaster.cache-lab.voxfor-lab. (
  2026081002 60 30 3600 8
)
@       IN NS   ns.cache-lab.voxfor-lab.
ns      IN A    192.0.2.53
node    IN A    192.0.2.10
node    IN AAAA 2001:db8::10
missing IN A    192.0.2.44
EOF

cp "$LAB/zone.v1" "$LAB/zone.live"
named-checkconf "$LAB/named.conf"
named-checkzone "$ZONE" "$LAB/zone.live"

Unbound forwards only the synthetic zone to the lab authority. Prefetch and serve-expired are disabled so neither feature obscures the eight-second observation. The 60-second local ceiling is above the zone’s requested lifetime and therefore does not shorten this fixture.

cat >"$UNBOUND_LAB/unbound.conf" <<EOF
server:
  verbosity: 1
  interface: 127.0.0.1@$RECURSOR_PORT
  do-ip4: yes
  do-ip6: no
  do-not-query-localhost: no
  access-control: 127.0.0.0/8 allow
  username: ""
  chroot: ""
  directory: "$UNBOUND_LAB"
  pidfile: "$UNBOUND_LAB/unbound.pid"
  logfile: "$UNBOUND_LAB/unbound.log"
  use-syslog: no
  module-config: "iterator"
  cache-min-ttl: 0
  cache-max-negative-ttl: 60
  prefetch: no
  serve-expired: no
remote-control:
  control-enable: no
forward-zone:
  name: "$ZONE."
  forward-addr: 127.0.0.1@$AUTH_PORT
EOF

unbound-checkconf "$UNBOUND_LAB/unbound.conf"
named -c "$LAB/named.conf" -g -u root >"$LAB/named.log" 2>&1 &
NAMED_PID=$!
unbound -d -c "$UNBOUND_LAB/unbound.conf" >"$UNBOUND_LAB/unbound.stdout" 2>&1 &
UNBOUND_PID=$!
sleep 1
kill -0 "$NAMED_PID" && kill -0 "$UNBOUND_PID"

Prime the Absence Before Publishing the Records

Query the recursive endpoint, not the authority, to create the two cache entries. Save the complete status and authority sections. The SOA in each response is the evidence that the negative answer has a cacheable lifetime.

NX_FIRST=$(dig @127.0.0.1 -p "$RECURSOR_PORT" \
  missing."$ZONE" A +noall +comments +authority)
ND_FIRST=$(dig @127.0.0.1 -p "$RECURSOR_PORT" \
  node."$ZONE" AAAA +noall +comments +authority)
printf '%s\n%s\n' "$NX_FIRST" "$ND_FIRST"

grep -q 'status: NXDOMAIN' <<<"$NX_FIRST"
grep -q 'status: NOERROR' <<<"$ND_FIRST"
NX_TTL_FIRST=$(awk '$4 == "SOA" { print $2; exit }' <<<"$NX_FIRST")
ND_TTL_FIRST=$(awk '$4 == "SOA" { print $2; exit }' <<<"$ND_FIRST")
[[ "$NX_TTL_FIRST" -ge 6 && "$NX_TTL_FIRST" -le 8 ]]
[[ "$ND_TTL_FIRST" -ge 6 && "$ND_TTL_FIRST" -le 8 ]]

sleep 2
NX_TTL_SECOND=$(dig @127.0.0.1 -p "$RECURSOR_PORT" \
  missing."$ZONE" A +noall +authority \
  | awk '$4 == "SOA" { print $2; exit }')
[[ "$NX_TTL_SECOND" -lt "$NX_TTL_FIRST" ]]
dig @127.0.0.1 -p "$RECURSOR_PORT" node."$ZONE" A +noall +answer

The last A query demonstrates NODATA’s type scope: an absent AAAA answer does not erase the existing A answer at node. The decreasing SOA TTL demonstrates that the resolver is serving a stored negative answer rather than repeatedly consulting the authority.

Make the Authority and Resolver Disagree on Purpose

Publish version 2 and reload only BIND. A direct authority query must see the new A and AAAA records. The same questions sent to Unbound must remain negative until the old cache entries expire. This is the exact split that makes a newly created production record appear “broken.”

cp "$LAB/zone.v2" "$LAB/zone.live"
named-checkzone "$ZONE" "$LAB/zone.live"
kill -HUP "$NAMED_PID"
sleep 1

AUTH_A=$(dig @127.0.0.1 -p "$AUTH_PORT" missing."$ZONE" A +short)
AUTH_AAAA=$(dig @127.0.0.1 -p "$AUTH_PORT" node."$ZONE" AAAA +short)
[[ "$AUTH_A" == 192.0.2.44 ]]
[[ "$AUTH_AAAA" == 2001:db8::10 ]]

NX_STALE=$(dig @127.0.0.1 -p "$RECURSOR_PORT" \
  missing."$ZONE" A +noall +comments +authority)
ND_STALE=$(dig @127.0.0.1 -p "$RECURSOR_PORT" \
  node."$ZONE" AAAA +noall +comments +authority)
grep -q 'status: NXDOMAIN' <<<"$NX_STALE"
grep -q 'status: NOERROR' <<<"$ND_STALE"
[[ -z "$(dig @127.0.0.1 -p "$RECURSOR_PORT" missing."$ZONE" A +short)" ]]
[[ -z "$(dig @127.0.0.1 -p "$RECURSOR_PORT" node."$ZONE" AAAA +short)" ]]

Wait beyond the original eight-second lifetime, then ask the same recursive resolver again. These assertions reject a partial result in which only one record type refreshes.

sleep 7
RECURSOR_A=$(dig @127.0.0.1 -p "$RECURSOR_PORT" missing."$ZONE" A +short)
RECURSOR_AAAA=$(dig @127.0.0.1 -p "$RECURSOR_PORT" node."$ZONE" AAAA +short)
[[ "$RECURSOR_A" == 192.0.2.44 ]]
[[ "$RECURSOR_AAAA" == 2001:db8::10 ]]
printf 'RECURSOR_MISSING_A=%s\nRECURSOR_NODE_AAAA=%s\n' \
  "$RECURSOR_A" "$RECURSOR_AAAA"

The reproduced run produced this representative receipt. Version numbers describe the test environment, not a requirement to pin those releases forever.

NAMED=BIND 9.20.26
UNBOUND=Version 1.22.0
NXDOMAIN_TTL_INITIAL=8
NXDOMAIN_TTL_AFTER_2S=6
NODATA_TTL_INITIAL=8
AUTH_MISSING_A=192.0.2.44
AUTH_NODE_AAAA=2001:db8::10
AUTHORITY_FRESH_WHILE_RECURSOR_NEGATIVE=yes
NXDOMAIN_SCOPE=name-and-class
NODATA_SCOPE=name-type-class
RECURSOR_MISSING_A=192.0.2.44
RECURSOR_NODE_AAAA=2001:db8::10
POST_EXPIRY_REFRESH=positive
CLEANUP=lab-absent

Accept the explanation only when the first response is NXDOMAIN for the missing name, the second is NOERROR with no requested AAAA data, both carry the expected SOA-derived TTL, the countdown falls, version 2 answers directly from the authority while the same resolver remains negative, both recursive answers become positive after expiry, and the guarded lab paths are absent after cleanup.

Translate the Lab Into a Production Check

Start by identifying who controls the zone. The domain, hosting, and DNS ownership boundaries matter because the registrar, DNS host, web host, and recursive resolver may be four different systems. Discover the authoritative name servers, then ask each one directly:

dig example.com NS +short
dig @ns1.provider.example new.example.com A +noall +comments +answer +authority
dig @ns2.provider.example new.example.com A +noall +comments +answer +authority
dig new.example.com A +noall +comments +answer +authority
dig @1.1.1.1 new.example.com A +noall +comments +answer +authority
dig @8.8.8.8 new.example.com A +noall +comments +answer +authority

Replace the placeholders with name servers returned by the parent delegation. If any authoritative server still returns the old zone, fix authority convergence first. Additional name servers help only when they carry the same serial and data; the secondary DNS convergence guide explains that boundary.

If every authority returns the new record but one recursive resolver returns a negative answer with an SOA countdown, the old negative cache is a strong explanation. Record the resolver IP, query name, type, class, response code, SOA owner, remaining TTL, and timestamp. Test the same resolver again after that TTL plus modest clock and network margin. Testing another public resolver can bound impact, but its success does not flush or invalidate the resolver your users actually reach.

When your organization controls the recursive resolver, a targeted cache removal may shorten recovery. Use the product’s documented name/type operation and capture before-and-after output; do not restart a shared resolver or clear its entire cache merely because one name is stale. When the resolver belongs to an ISP, enterprise network, VPN, browser secure-DNS provider, or public DNS operator, waiting for the declared lifetime is usually the predictable path.

If DNS operations belong to your infrastructure provider, managed DNS and server support is relevant only when the provider accepts ownership of authoritative checks, resolver evidence, change timing, and the post-expiry receipt—not simply because the article mentions DNS.

Plan Record Creation Before the First Negative Query

Lowering a positive record’s TTL does not retroactively shorten an NXDOMAIN response already stored by a recursive resolver. The negative timer came from the SOA values in the response that resolver received. Changing the SOA now helps only future negative answers after authority propagation and after earlier cached negatives expire.

For a planned launch, publish required names before clients, certificate authorities, mail systems, or monitors begin querying them. Confirm every authority serves the intended serial and record. If you must shorten negative caching, adjust the SOA policy far enough in advance for old negative entries to age out, then restore an appropriate production value after the launch window.

TTL design also belongs to the failure model. Very short values increase query load and do not make every layer refresh instantly. Longer values reduce authority traffic but extend planned change windows. The same tradeoff appears in Anycast and GeoDNS failover design: define which failure a DNS change can route around and how you will observe convergence.

Keep the Recovery and Cleanup Scoped

The lab changes no system service configuration, but it starts two foreground processes and creates two exact directories. Stop only the recorded PIDs, confirm the literal paths, and then remove those paths. In production, rollback means restoring the previous zone version and serial only when the new record itself is wrong; rollback does not make already cached answers disappear.

cleanup
trap - EXIT
[[ ! -e "$LAB" && ! -e "$UNBOUND_LAB" ]]
echo 'CLEANUP=lab-absent'

A safe production change retains the pre-change zone export, records old and new serials, validates the zone before reload, checks every authoritative server, and names the person who can reverse it. If the new data is correct and only an external recursive cache is stale, reversing the authority would replace a valid destination with an old one while leaving the cached negative answer untouched.

FAQ: DNS Negative Caching

How long can NXDOMAIN stay cached?

Start with the smaller of the SOA record TTL and SOA MINIMUM value in the negative response. Then account for resolver-specific caps, eviction, prefetch, serve-stale behavior, and any client-side cache. The remaining SOA TTL shown by the affected resolver is more useful than assuming the zone’s current policy was the one originally cached.

Does lowering the new record’s TTL clear an old NXDOMAIN?

No. The record did not exist in the cached response, so its new positive TTL was not part of that response. The old negative entry ages according to the SOA-derived lifetime and the recursive resolver’s policy.

Why does dig work against the name server but fail normally?

A direct dig @authoritative-server bypasses recursion and reads current authority data. A normal lookup usually reaches a recursive resolver, which may correctly reuse an earlier negative answer until expiry. Save both outputs with timestamps to prove the split.

Can one resolver work while another still returns NXDOMAIN?

Yes. Recursive resolvers have independent caches populated at different times. One may never have seen the negative answer, another may have expired it, and a third may still have seconds remaining. Compare the exact resolver addresses rather than treating “the internet” as one cache.

Should I flush DNS on the web server or my laptop?

Only after identifying the component that returned the stale answer. A local flush cannot clear an ISP or public recursive cache, and a resolver flush cannot clear an application’s own cache. Broad restarts add unrelated risk; prefer a targeted, documented removal on a resolver you control.

Is SERVFAIL negative caching?

It is a failed resolution outcome, but it is not an authoritative assertion that the name or record type does not exist. Check delegation reachability, authority responses, DNSSEC validation, and resolver logs. Repairing the failure path is different from waiting for an NXDOMAIN or NODATA lifetime.

End With a DNS Receipt, Not “Propagation”

“DNS propagation” is too vague to close an incident. Preserve four facts: every authoritative server’s answer and serial, the affected recursive resolver’s response code, the remaining negative TTL and SOA, and the first timestamp when that same resolver returns the positive record. Those fields show whether the problem was unpublished authority data, inconsistent authorities, a cached negative answer, or another failure entirely.

Once the authority is consistent and the resolver’s negative TTL has expired, the new positive answer is the result that matters. Until then, a valid new record and a valid old cache can disagree without either DNS component malfunctioning.

Share this Post

Leave a Reply

Your email address will not be published. Required fields are marked *