Internet Protocols · DNS
Negative Caching: NXDOMAIN, NODATA, and the SOA Minimum
How a resolver distinguishes a name that does not exist from a name with no records of the requested type, and how the SOA fields set the lifetime of both.
The protocol has two different ways of saying no, and they mean different things. One says the name does not exist anywhere in the namespace. The other says the name exists but holds no records of the type requested. They arrive with the same response code and differ only in whether the authority section carries an SOA record, which makes the distinction easy to miss and easy to implement wrong.
Two negatives, one response code
A query for missing.example.org at the zone’s authoritative server returns RCODE 3, NXDOMAIN, with the zone’s SOA in the authority section. A query for www.example.org with type MX, where www.example.org has only A and AAAA, returns RCODE 0, NOERROR, with an empty answer section and the zone’s SOA in the authority section. The second case is called NODATA.
The difference matters to anything that reacts to the answer. A name that does not exist cannot have subdomains either, which is the RFC 8020 rule and a real constraint on what a server may answer, so an NXDOMAIN is a statement about a whole subtree. A NODATA answer says nothing about other types at the same name, and nothing at all about names beneath it. A client that conflates them will either conclude that a name is absent when only one record type is, or will keep retrying a type that will never appear.
Both negatives must carry the SOA of the zone that made the claim, because that SOA is the only record in the response that identifies which zone’s authority the negative rests on. A negative answer without an SOA cannot be cached under the standard rules and is treated as an uncacheable response, which is why a misconfigured server that omits it causes a resolver to re-query on every lookup.
Why the SOA appears in a negative answer
The SOA record carries the negative TTL in two fields that predate negative caching. The SOA’s own TTL is the TTL of the SOA RRset; the MINIMUM field inside the SOA RDATA was defined in RFC 1035 as a floor on the TTL exported with every record in the zone, and in practice it was used as the default TTL for records that did not state one. When negative caching was standardised in RFC 2308, the MINIMUM field was redefined as the negative cache TTL, and the earlier meanings were deprecated.
The rule that resulted is a minimum of two values: the negative TTL is the lesser of the SOA’s TTL and the SOA’s MINIMUM field. The reason for the double bound is that a zone might set MINIMUM to a large value for legacy reasons while giving the SOA a short TTL to allow frequent updates. Without the second bound, the negative answer would outlive the SOA it was derived from, and a resolver could hold a stale “does not exist” long after the name was created.
A resolver that caches a negative answer keys it by the query name and class, and for NODATA also by the type. The type is part of a NODATA key because an answer for MX says nothing about A. An NXDOMAIN applies to every type at the name, so it is stored once per name and class and used for all types, but only if the resolver also remembers that the name does not exist for the purposes of the subtree, which is what makes the NXDOMAIN cache entry a different kind of object from an RRset cache entry.
The lifetime is decremented like any TTL. A cached negative answer served from the cache has a TTL field equal to the remaining lifetime, and a resolver must not serve a cached negative with a TTL greater than the original. Because the negative answer’s TTL is derived rather than stored on the record, implementations that synthesise an SOA for the cached response have to reconstruct it with the remaining time, which is a common source of off-by-one errors in TTL handling.
The RFC 8020 rule and what it forbids
A server that returns NXDOMAIN for a name is asserting that no name in the subtree exists. That means a server cannot return NXDOMAIN for example.org while also holding www.example.org, because the two statements contradict. RFC 8020 made this explicit because RFC 1034 had failed to distinguish empty non-terminals from nonexistent names, and resolvers did not treat a cached NXDOMAIN as covering names beneath it.
An empty non-terminal is a name that has no records of its own but has descendants that do. sub.example.org might hold no records while host.sub.example.org does. The name sub.example.org exists in the namespace, because it is an ancestor of an existing name, so a query for it must return NODATA, not NXDOMAIN. Getting this wrong breaks the rule above and produces contradictory answers depending on which server a resolver happens to ask.
The same rule constrains wildcards. A wildcard synthesises answers for names that do not exist, but a name that exists as an empty non-terminal is not synthesised by the wildcard, because the wildcard only applies to names that do not exist. A zone with *.example.org A 192.0.2.1 and a descendant host.sub.example.org A 192.0.2.2 must answer NODATA for sub.example.org rather than synthesising the wildcard’s address, because sub.example.org exists as an ancestor. This is the interaction RFC 4592 addresses at length.
How a cache answers a negative from a wildcard zone
Wildcard zones complicate negative caching because the response a resolver receives may be a synthesised positive answer whose owner name is the query name. That answer is cacheable as a normal RRset under the queried name, even though the zone file contains no such record. The signature on it, if DNSSEC is in use, is made by the wildcard’s owner, and the response carries the wildcard’s RRSIG with a label count lower than the query name’s, which is how a validator knows the answer was synthesised.
The negative side of a wildcard zone is that a query for a type the wildcard does not provide gets NODATA from the wildcard’s zone, with the SOA of that zone. Without DNSSEC, a resolver cannot distinguish a name that exists but has no record of the requested type from a name that does not exist but is covered by a wildcard that also lacks that type, because both produce a NODATA response with the same shape. That ambiguity is a property of wildcards and is one reason they are used sparingly where the distinction matters.
Aggressive use and the cost of a long negative TTL
A long negative TTL reduces query volume for names that genuinely do not exist, which matters because resolvers are repeatedly asked for names that never resolve: misconfigured clients, mistyped hostnames, and names that only ever appear in malware callbacks. Caching those answers prevents the resolver from walking the tree on every attempt.
The cost is that creating a name after a negative answer has been cached leaves it unreachable for the remainder of the TTL. Provisioning a new host and finding it does not resolve because a client’s resolver cached the absence is the standard operational surprise. The fix is to keep the SOA MINIMUM short during periods of change and to accept a higher query rate, or to avoid querying a name before it exists. There is no in-band invalidation: the protocol has no way for a zone to say that cached data about this name should be discarded.
DNSSEC makes the trade sharper. Proving a negative with signatures requires the response to carry the records that prove absence, which for a zone with many names can be several records and a signature each. The size of that proof is covered in Authenticated Denial of Existence with NSEC and NSEC3, and the practical effect is that signed negative answers are larger and more likely to hit the truncation boundary described in EDNS(0) and the Truncation Boundary.
def negative_ttl(soa_ttl, soa_minimum):
"""RFC 2308: the negative TTL is the lesser of the two SOA-derived bounds."""
return min(soa_ttl, soa_minimum)
def classify(resp):
if resp.rcode == 3:
return "NXDOMAIN" # name and whole subtree absent
if resp.rcode == 0 and not resp.answer and resp.authority_soa:
return "NODATA" # name exists, this type does not
if resp.rcode == 0 and not resp.answer:
return "EMPTY" # no SOA: not cacheable as a negative
return "POSITIVE"
The EMPTY case is worth keeping distinct. A NOERROR response with no answer and no SOA is not a negative answer under the standard; it is a response the resolver cannot interpret, and caching it under the negative rules would invent a TTL the server never supplied.
Stale answers as a deliberate relaxation
Serving expired data is normally forbidden, but RFC 8767 defines a stale-answer mode: an expired cache entry is served while a refresh is attempted, with the TTL in the response set to a small value, 30 seconds being the recommended one. The motivation is availability. If the authoritative servers for a zone are unreachable, serving a slightly stale address is better than returning a failure. The mode is for that case only, since a resolver that preferred stale data unconditionally would never notice a change, and a stale answer must not be cached downstream as fresh.
For negative answers the same relaxation is more dangerous, because a stale NXDOMAIN can keep a newly created name unreachable, and stale negative records widen the window in which a newly published DS or TLSA record goes unseen. Implementations that serve stale data therefore bound how long they will do it, and some exclude negatives; that choice is policy rather than protocol.
The underlying asymmetry is that a positive answer that is slightly wrong usually still connects to something, while a negative answer that is slightly wrong refuses to connect at all. That difference in consequence is why negative TTLs are usually configured well below positive TTLs even though the protocol treats them through the same countdown machinery.