Negative Caching and the SOA Minimum TTL

After working through this guide you will be able to read the negative-cache TTL out of any zone, predict exactly how long a failed lookup will keep failing after you fix it, tune the SOA safely on Route 53, Cloudflare, Cloud DNS and BIND, and watch a cached “no” drain out of a resolver in real time.

Positive answers get all the attention. Every operator knows a record’s TTL controls how long resolvers hold it. Far fewer know that the absence of a record is cached too, on a completely different timer, controlled by a field in a record most people have never edited. That asymmetry is why a five-minute typo can produce an hour of user-visible failures, and why a subdomain you created twenty minutes ago still returns NXDOMAIN to the one monitoring system whose resolver happened to look it up first.

Key objectives:

  • Compute the negative-cache duration as min(SOA MINIMUM, SOA record TTL) and know why both halves matter.
  • Tell NXDOMAIN from NODATA, and understand why clients behave differently for each.
  • Change the SOA minimum on the major providers without breaking zone transfers or DNSSEC.
  • Sequence subdomain creation and ACME challenges so a negative answer is never cached in the first place.

What RFC 2308 actually specifies

When an authoritative server has no answer, it returns an empty answer section plus its own SOA record in the authority section. That SOA is not decoration — it is the instruction telling the resolver how long to remember the failure. RFC 2308 defines the negative-cache duration as the lesser of the SOA MINIMUM field and the TTL on the SOA record itself. Either value can be the binding constraint, which is the part that catches people: setting MINIMUM to 300 achieves nothing if the SOA record is published with a TTL of 60, and setting the SOA TTL to 60 achieves nothing if MINIMUM is 3600 and the resolver is one of the many that reads only what it is given.

Deriving the negative-cache TTL from the SOA An SOA record line feeds two callouts, its own record TTL and its MINIMUM field, which combine through a minimum function into the duration a resolver caches a negative answer. Where the negative TTL comes from example.com. 900 IN SOA ns1.example.com. hostmaster.example.com. ( 2026072801 7200 900 1209600 300 ) SOA record TTL = 900 a hard ceiling on how long the negative answer may be held MINIMUM field = 300 since RFC 2308 this is the negative TTL, not a zone default min(900, 300) = 300 Negative answer cached for 300 seconds

The other four SOA fields — serial, refresh, retry, expire — never reach a recursive resolver’s caching logic at all. They coordinate zone transfers between primary and secondary authoritative servers, and no resolver ever reads them. The MINIMUM name is a historical accident: before RFC 2308 it was the default TTL for records in the zone, a role now filled by $TTL. Reading an old zone file with that assumption in mind is how the field ends up at 86400.

NXDOMAIN is not NODATA

Two different “no” answers travel over the wire, and clients react to them very differently.

NXDOMAIN versus NODATA A query reaches a decision point asking whether the owner name exists; an absent name yields NXDOMAIN with RCODE 3, while a present name lacking the requested type yields NODATA with RCODE 0 and an empty answer section. Two ways for a zone to say no Query arrives for a name in your zone Does the owner name exist at all? name absent name present NXDOMAIN RCODE 3: no name here, and no child names below it NODATA RCODE 0, zero answers: the name exists, this type does not Both are cached against the same SOA-derived timer, but clients treat them differently.

NXDOMAIN (RCODE 3) asserts that the queried name does not exist for any type, and — per RFC 8020 — that nothing exists beneath it either. A resolver that caches an NXDOMAIN for example.com is entitled to answer NXDOMAIN for api.example.com without asking anyone. Clients treat it as terminal: a mail server stops trying, a browser shows a DNS error, a service-discovery library gives up rather than falling back.

NODATA is RCODE 0 with an empty answer section and the SOA in the authority section. It says the name exists but has no record of the type you asked for. Clients treat this as routine and keep going: a dual-stack client that gets NODATA for AAAA immediately tries A and connects. That is exactly the desired behavior, which is why publishing an IPv6-less hostname is harmless while publishing a typo’d hostname is not. When you are staging the dual-stack work described in How to Configure A vs AAAA Records for IPv6 Migration, that distinction is doing quiet work in your favor.

The practical consequence: the cost of a cached negative answer scales with which one you produce. A cached NODATA for AAAA costs nothing. A cached NXDOMAIN for your API hostname is an outage for every client behind that resolver, for the full negative TTL, no matter how quickly you fixed the zone.

How a large minimum turns a small mistake into an incident

A five-minute typo under a one-hour negative TTL A typo is live for five minutes, but the NXDOMAIN it produced stays cached for a further sixty minutes, so clients only recover at the sixty-five minute mark. A five-minute typo, an hour of failures Zone published with SOA MINIMUM = 3600 typo live 5m NXDOMAIN cached 3600s by every resolver that already asked 0 min 5 min 65 min Zone fixed at 5 minutes authoritative data is correct Clients recover at 65 minutes when the cached no expires

There is no purge button. Recursive resolvers do not accept invalidation from authoritative servers, so once a negative answer is cached the only options are waiting it out or telling affected users to change resolver. This is the reverse of the situation with a positive record, where you can at least pre-lower the TTL before a planned change — the technique described in Lowering TTLs Safely Before a Provider Migration. Negative caching gives you no such lever after the fact, so the entire discipline is preventive.

Sensible values: 300 seconds for a zone under active change, 900 for a stable production zone, and nothing above 3600 for anything. Going below 60 is counterproductive — every miss becomes an authoritative query, and a zone under a random-subdomain flood will feel it.

Prerequisites

You need dig (BIND 9.16 or newer prints the authority section clearly with +noall +authority) and edit access to the zone. Confirm which nameservers are authoritative before you start, so you can query them directly rather than through a cache:

dig example.com NS +short

Step-by-step procedure

Step 1 — Read the current negative TTL

Ask an authoritative server for the SOA and read two numbers: the TTL in the second column, and the last value in the parentheses.

dig @ns1.example.com example.com SOA +noall +answer

Expected output:

example.com.		900	IN	SOA	ns1.example.com. hostmaster.example.com. 2026072801 7200 900 1209600 300

Here the record TTL is 900 and MINIMUM is 300, so negative answers live for 300 seconds. Confirm it by triggering one and reading the authority section, which is where a resolver gets the number from:

dig @ns1.example.com definitely-not-a-real-name.example.com A +noall +authority +comments \
  | grep -E 'status|SOA'

Expected output:

;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 51042
example.com.		300	IN	SOA	ns1.example.com. hostmaster.example.com. ...

Note that the SOA’s TTL in the authority section of a negative answer is already the effective negative TTL — the authoritative server does the min() for you.

Step 2 — Change it on your provider

Each platform exposes the field differently, and one of them does not expose it at all.

# AWS Route 53 — the last field in the value string is MINIMUM
aws route53 change-resource-record-sets --hosted-zone-id "$HOSTED_ZONE_ID" \
  --change-batch '{"Changes":[{"Action":"UPSERT","ResourceRecordSet":{
    "Name":"example.com","Type":"SOA","TTL":900,
    "ResourceRecords":[{"Value":"ns-1234.awsdns-56.org. awsdns-hostmaster.amazon.com. 1 7200 900 1209600 300"}]}}]}'

# Google Cloud DNS — rewrite the whole rrdata string
gcloud dns record-sets update example.com. --type=SOA --zone=prod-zone --ttl=900 \
  --rrdatas="ns-cloud-a1.googledomains.com. cloud-dns-hostmaster.google.com. 1 21600 3600 259200 300"

# Azure DNS — a dedicated flag for the field
az network dns record-set soa update --resource-group dns-rg \
  --zone-name example.com --minimum-ttl 300
; BIND / PowerDNS — edit the zone, bump the serial, reload
$TTL 900
@   IN  SOA ns1.example.com. hostmaster.example.com. (
        2026072802  ; serial — must increase or secondaries ignore the change
        7200        ; refresh
        900         ; retry
        1209600     ; expire
        300 )       ; MINIMUM — the negative-cache TTL

Cloudflare is the exception: it generates and manages the SOA itself, and the minimum is not editable on standard plans. Read what it publishes with dig and plan around the value rather than trying to change it. If your zone genuinely needs a shorter negative TTL than the provider allows, that is a reason to weigh the trade-offs in Apex Domain Setup on Cloudflare versus Route 53.

Whatever platform you use, the serial must increase. On a signed zone the SOA change is re-signed automatically by any managed provider, but on a self-hosted signed zone the SOA is covered by an RRSIG that must be regenerated — bump, sign, then reload.

Step 3 — Watch a cached negative answer drain

This is the demonstration that makes the concept stick. Query a name that does not exist through a public resolver, then query it again and watch the SOA’s TTL in the authority section count down.

NAME="drain-test-$RANDOM.example.com"
for i in 1 2 3; do
  dig @1.1.1.1 "$NAME" A +noall +authority | awk '{print $2, $1}'
  sleep 20
done

Expected output:

300 example.com.
280 example.com.
260 example.com.

The countdown is the resolver telling you how much longer it will refuse to look this name up again. Create the record now and re-query: you will still get NXDOMAIN until that counter hits zero, from that resolver only. Ask a resolver that never saw the failure and the new record appears instantly — which is precisely why “it works for me” is meaningless during a rollout.

Step 4 — Sequence rollouts so nothing caches a no

The fix for negative caching is ordering, not tuning. Create the record before anything can query it, and verify it resolves everywhere before you point traffic, monitoring, or a certificate authority at it.

  1. Create the record with a short TTL.
  2. Verify it answers on several public resolvers.
  3. Only then add it to load balancer config, monitoring, or client configuration.
  4. Raise the TTL once the name is stable.

The order gets inverted constantly by automation: a deploy pipeline registers a health check against new-service.example.com in the same step that creates the record, the health checker’s resolver wins the race, and the service is marked down for the full negative TTL. Separate the two steps with a verification gate.

ACME challenges and the DNS-01 race

DNS-01 validation is the worst case for negative caching because someone else’s resolver does the querying. Your ACME client writes _acme-challenge.example.com TXT and immediately tells the CA to validate. If anything queried that name earlier — a previous failed issuance attempt, a monitoring probe, the ACME client’s own pre-flight check — the CA’s resolver may hold an NXDOMAIN or NODATA for it and the validation fails with “no TXT record found” while dig from your laptop shows the record plainly.

Most ACME clients have a propagation delay setting for exactly this. Set it above your negative TTL, not below:

certbot certonly --dns-route53 \
  --dns-route53-propagation-seconds 330 \
  -d example.com -d '*.example.com'

Check the name from a resolver that has never seen it before submitting:

dig @9.9.9.9 _acme-challenge.example.com TXT +short

A cached negative answer is one of several DNS-shaped reasons an issuance fails; the full triage sequence is in Troubleshooting CAA and DNS-01 Validation Failures, and the automation patterns that avoid the race entirely are covered in Automating ACME DNS-01 Challenges for Wildcard Certificates.

Troubleshooting

The record exists but clients still get NXDOMAIN

Confirm the authoritative answer first, then the cached one, and compare:

dig @ns1.example.com api.example.com A +short        # authoritative truth
dig @8.8.8.8 api.example.com A +noall +comments | grep status
dig @8.8.8.8 example.com SOA +noall +authority       # remaining negative TTL

If the authoritative server answers and the public resolver returns NXDOMAIN, you are waiting out a cached negative. There is nothing to fix and nothing to purge — note the remaining TTL, tell whoever is asking how long it will be, and use the incident as the argument for lowering MINIMUM. If the authoritative server also returns NXDOMAIN, the record is not where you think it is: check for a trailing-dot mistake that appended the zone name twice, or a record created in the wrong hosted zone when a domain has duplicate zones.

A resolver enforces its own negative TTL floor

Some resolvers clamp negative caching independently of your SOA — a floor of 60 or 300 seconds is common, and a few enterprise forwarders cache negatives far longer than they should. Detect it by comparing what you publish against what a specific resolver reports on a fresh negative answer. If the reported authority TTL exceeds your MINIMUM, that resolver is applying its own policy and lowering the zone value further will not help. Verify convergence against a spread of resolvers rather than one, and treat the slowest as your real recovery time.

A wildcard record masks NXDOMAIN, until it does not

A *.example.com record means almost nothing under the zone is ever NXDOMAIN — every unmatched name gets the wildcard’s answer instead. That looks convenient and hides typos beautifully: a request for aip.example.com resolves fine and lands on the wrong backend, and no DNS error ever appears. Worse, wildcards do not expand where an explicit name already exists at that label for any type, so adding a single TXT record at api.example.com stops the wildcard from covering api and turns a previously-working A lookup into NODATA. Audit with:

dig @ns1.example.com nonexistent-probe.example.com A +noall +answer

An answer here means a wildcard is in play. Know that before you rely on NXDOMAIN as a signal for anything.

DNSSEC caches the proof of absence, not just the absence

On a signed zone the negative answer carries NSEC or NSEC3 records proving no name exists in a given range, along with their RRSIGs. Resolvers cache that proof under the same SOA-derived timer, and a validating resolver may use one cached NSEC record to synthesize NXDOMAIN for other names in the proven range without querying at all. The practical effect is that negative caching on a signed zone can be broader than you expect: creating a name inside a range already proven empty stays invisible until the proof expires. NSEC3 with opt-out narrows this; aggressive use of the cache, as specified in RFC 8198, widens it. If negative answers persist past your MINIMUM on a signed zone, check whether the resolver is validating and reproduce with +cd to rule the signing chain in or out — the same first move used in Debugging DNSSEC Validation Failures.

The SOA changed but resolvers still use the old minimum

Resolvers cache the SOA record itself. Lowering MINIMUM from 3600 to 300 takes effect only after each resolver’s cached SOA expires, which happens on the SOA record’s own TTL. Lower the SOA TTL first, wait it out, then lower MINIMUM — the same two-step sequencing every TTL reduction requires. Confirm the change landed authoritatively before blaming caches:

for ns in $(dig example.com NS +short); do
  echo "== $ns"; dig @"$ns" example.com SOA +short
done

All authoritative servers must agree. A secondary still serving the old SOA means the zone transfer did not run, usually a serial that was not incremented.

Frequently Asked Questions

What is the difference between the SOA minimum and the zone default TTL? They were the same thing before RFC 2308 and are unrelated now. The zone default TTL is $TTL in a zone file and supplies a TTL to records that do not declare one. MINIMUM is the last SOA field and controls only how long resolvers cache a negative answer. Finding MINIMUM at 86400 usually means someone copied an old zone file that still used it as a default.

Can I purge a cached NXDOMAIN from a public resolver? No. Recursive resolvers accept no invalidation signal from authoritative servers, so a cached negative answer must expire on its own timer. That is why the mitigation is preventive — keep MINIMUM low and create records before anything queries them.

Why does my new subdomain work from one machine but not another? Different resolvers, different cache states. Only the resolvers that queried the name before it existed hold a negative answer; those that never asked get the fresh record immediately. Check the remaining negative TTL on the failing path with dig @<resolver> <zone> SOA +noall +authority.

Does NODATA cause the same problem as NXDOMAIN? It is cached the same way but hurts far less, because clients treat it as a routine “not this type” and fall back. A cached NODATA for AAAA simply sends the client to IPv4; a cached NXDOMAIN tells the client the whole name is gone and it stops.

What value should the SOA minimum be? 300 seconds while a zone is under active change, 900 for a stable production zone, and never above 3600. Below 60 is counterproductive: every miss becomes an authoritative query and a random-subdomain flood turns into load on your nameservers.

Back to Propagation & Caching Basics