is it dns? / wiki ← back to the live monitor

When it actually is DNS

Stage 3: "it was DNS." Field-tested failure modes, each paired with the dig output that proves it.

1. Stale TTL: the change you made an hour ago hasn't propagated#

Symptom: "I updated the record. It still resolves to the old value."

Diagnostic: check the TTL the resolver hands you. If it's small and counting down, you're being served fresh. If it's large and the answer is wrong, your resolver cached the old one.

dig example.isitdns.net @1.1.1.1 | grep -A1 ";; ANSWER"
;; ANSWER SECTION:
example.isitdns.net.  220     IN      A       192.0.2.1

The TTL is counting down: example.isitdns.net publishes a 300 s TTL, so an answer showing 220 was cached 80 seconds ago.

nslookup example.isitdns.net 1.1.1.1
Non-authoritative answer:
Name:   example.isitdns.net
Address: 192.0.2.1

The Non-authoritative answer: label confirms this is a cached response from your resolver, not a fresh answer from the authority.

Workaround: use dig +trace name to walk the full resolution chain, or query the authoritative server directly. example.isitdns.net is hosted on Cloudflare, so:

dig example.isitdns.net @coleman.ns.cloudflare.com
nslookup example.isitdns.net coleman.ns.cloudflare.com

That bypasses every cache in the chain. See dig-flags for +trace and related options.

Real fix: lower the TTL on the record before you plan to change it. Wait one TTL-worth. Then change it. Then raise the TTL back.

2. Negative caching: NXDOMAIN cached longer than you expect#

Symptom: "I just created the record but it returns NXDOMAIN."

The cause: before the record existed, your resolver asked, was told "no such name," and cached the negative answer for the SOA minimum value (often 1800 seconds or more). This is called negative caching and is defined in RFC 2308.

dig SOA isitdns.net @1.1.1.1
isitdns.net.   1800  IN  SOA  coleman.ns.cloudflare.com. dns.cloudflare.com. \
  2409741473 10000 2400 604800 1800
nslookup -type=SOA isitdns.net
isitdns.net
        origin = coleman.ns.cloudflare.com
        mail addr = dns.cloudflare.com
        serial = 2409741473
        refresh = 10000
        retry = 2400
        expire = 604800
        minimum = 1800

The serial (2409741473 above) will differ when you run this. Cloudflare increments it on every zone change. The minimum field (1800 here, last number in the dig output) is the negative-cache TTL. Strictly, the negative TTL is the lower of the SOA's own TTL and the minimum field (RFC 2308 §3); Cloudflare publishes both as 1800. A resolver that was told "no such name" won't ask again for 1800 seconds even after you create the record.

Workaround: flush the resolver, query a different resolver, or wait it out.

3. Missing AAAA: only happens on IPv6#

Symptom: "The site is up. But this one user can't reach it."

The cause: that user's stub has IPv6 connectivity. Your A is correct but you haven't published AAAA. Happy Eyeballs (RFC 8305) tries both, but some apps don't, or the v6 path is broken between the user and you.

Demo status: the probe.isitdns.net demo zone is being rebuilt and its delegation is withdrawn, so host.baseline and host.dual do not answer right now (today both queries come back as an empty NOERROR from the parent zone). The transcripts below are what the healthy demo returns; the A-only vs dual-stack mechanics are unchanged.

host.baseline.probe.isitdns.net is deliberately A-only for exactly this demo: it answers with an A and nothing for AAAA.

dig +short A    host.baseline.probe.isitdns.net
dig +short AAAA host.baseline.probe.isitdns.net
192.0.2.1

The first command returns 192.0.2.1. The second returns nothing (empty answer, NOERROR status): the name exists but has no AAAA record.

nslookup -type=A    host.baseline.probe.isitdns.net
nslookup -type=AAAA host.baseline.probe.isitdns.net

If A returns and AAAA returns empty, dual-stack clients are fine: they fall back to v4. The trap is the dual-stack name where AAAA also answers but the v6 path is broken. The host.dual.probe.isitdns.net name is the contrast: it publishes both, so an IPv6-only client gets an address it may not be able to reach.

dig +short A    host.dual.probe.isitdns.net
dig +short AAAA host.dual.probe.isitdns.net
192.0.2.20
2001:db8::20

4. CNAME at the apex#

Symptom: "The zone broke when I added the CNAME on example.com itself."

The cause: the apex of a zone must hold SOA and NS records. A CNAME cannot coexist with any other record at the same name (RFC 1034 §3.6.2); in signed zones, DNSSEC's own RRSIG and NSEC records are the one carve-out (RFC 4035 §2.5). So example.com CNAME elsewhere.com is illegal.

; This is illegal. A real authoritative will reject:
example.com.  300 IN CNAME target.tld.
example.com.  300 IN SOA   ...
example.com.  300 IN NS    ...

Workaround: some DNS providers offer "CNAME flattening": they resolve the target server-side and return A records at the apex. That is a vendor feature, not standard DNS.

5. Missing glue records#

Symptom: "I delegated sub.example.com to ns1.sub.example.com and now nothing resolves."

The cause: the parent zone delegates sub to ns1.sub.example.com, but to find ns1.sub.example.com, the resolver needs to ask ns1.sub.example.com. Chicken, meet egg. Glue records are A/AAAA records the parent includes in its referral response so the resolver can bootstrap. See delegations for how referrals work.

Diagnostic:

dig +trace sub.example.com

Look for the referral step in the trace output. If the additional section of the referral response is empty, the glue records are absent.

nslookup has no +trace equivalent. Use dig-along or dig-flags for tracing options. PowerShell can do the bare lookup, but it will not show the referral chain:

Resolve-DnsName sub.example.com

6. Negative response → wrong rcode#

rcodeNameWhat it means
0NOERRORAnswer follows (might be empty if no records of that type exist)
2SERVFAILThe resolver gave up: auth was unreachable, DNSSEC failed, etc.
3NXDOMAINThe name doesn't exist (and won't, for the SOA minimum window)
5REFUSEDPolicy refusal (RFC 1035 §4.1.1). The classic case: an authoritative server asked about a zone it does not host
9NOTAUTH"Server not authoritative for zone." A dynamic-update and TSIG rcode (RFC 2136); you rarely see it on plain lookups
dig nonexistent.invalid | grep "status:"
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 12345
nslookup nonexistent.invalid
** server can't find nonexistent.invalid: NXDOMAIN

NOERROR with zero answers is not the same as NXDOMAIN. The first means "the name exists but no record of that type." The second means "the name doesn't exist at all." Caches handle them differently: see anatomy-of-a-query for how resolvers interpret each code.

DNSSEC-signed Cloudflare zones almost never say NXDOMAIN. Ask for a name that does not exist in a signed Cloudflare-hosted zone and you get NOERROR with an empty answer, not NXDOMAIN. Cloudflare synthesizes a "no such record" NSEC at the queried name, the "black lies" technique (draft-valsorda-dnsop-black-lies), and that answer is NOERROR by design. dig nonexistent.isitdns.net proves it: status: NOERROR, zero answers, even though nothing is configured there. Unsigned Cloudflare zones are not affected and still return a real NXDOMAIN. That is why the demo above uses the reserved .invalid TLD: to see a real NXDOMAIN you have to ask an authority that returns one.

Demo status: probe.isitdns.net is a plain BIND delegation whose server is being rebuilt. While the delegation is withdrawn, names under it are answered by the parent Cloudflare zone, so today they return the synthesized empty NOERROR described above. Once it is back, any name that does not exist under it returns a real NXDOMAIN:

dig anything.does-not-exist.probe.isitdns.net | grep "status:"
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 12345

Practical fallout: monitoring or mail logic that keys on NXDOMAIN misfires against signed Cloudflare zones but works as written against a stock authority like that one.

7. UDP truncated → fell back to TCP → blocked by firewall#

Symptom: "Some queries work, some don't. The big ones don't."

The cause: DNS over UDP caps at 512 bytes by default (RFC 1035 §2.3.4); EDNS0 (RFC 6891) lifts it, and 1232 bytes is what most modern resolvers advertise, Cloudflare included. If the response is bigger, the server sets the truncated (TC) flag and the resolver retries over TCP/53. If your firewall drops outbound TCP/53, the retry fails silently. See dig-flags for +ignore, +tcp, and buffer-size options.

txt.example.isitdns.net is a live TXT record you can use to practice the TCP/UDP flag pair:

dig +ignore TXT txt.example.isitdns.net
dig +tcp TXT txt.example.isitdns.net

+ignore tells dig to accept the UDP response as-is rather than retrying over TCP. +tcp forces TCP outright, which is what a resolver would do after receiving a TC flag.

nslookup cannot show the TC bit directly, but -vc (virtual circuit) forces TCP:

nslookup -type=TXT txt.example.isitdns.net
nslookup -vc -type=TXT txt.example.isitdns.net

8. DNSSEC validation failed#

Symptom: "I see SERVFAIL but the authoritative server is up."

The cause: the resolver tried to validate DNSSEC and the chain didn't verify. Common causes: bad key rollover, missing DS at parent, system clock skew (signatures have a validity window), or a CDN serving stale signed data. See dnssec-troubleshooting for a systematic approach.

The most reliable way to check validation is with the Cloudflare DoH API, which returns the AD (Authenticated Data) bit in JSON and is not subject to local network interception:

curl -s -H 'accept: application/dns-json' \
  'https://cloudflare-dns.com/dns-query?name=isitdns.net&type=A'
{
  "Status": 0, "AD": true,
  "Answer": [
    {"name": "isitdns.net", "type": 1, "TTL": 300, "data": "104.21.56.211"},
    {"name": "isitdns.net", "type": 1, "TTL": 300, "data": "172.67.155.242"}
  ]
}

"AD": true means the validating resolver confirmed the DNSSEC chain end to end. isitdns.net is fully signed with a DS record at .net, so it always validates. The two A records are the Cloudflare anycast pair; any resolver round-trip may return them in either order.

For a deliberately broken zone, the same endpoint returns SERVFAIL (Status 2):

curl -s -H 'accept: application/dns-json' \
  'https://cloudflare-dns.com/dns-query?name=dnssec-failed.org&type=A'
{"Status": 2, "AD": false,
 "Comment": ["EDE(9): DNSKEY Missing no SEP matching the DS found for dnssec-failed.org."]}

Status 2 is SERVFAIL. The Extended DNS Error (EDE) code 9 names the exact fault: no KSK matching the DS at the parent. That is the validation failure message a resolver returns when the chain breaks.

With dig, request DNSSEC data using +dnssec. The response will include an RRSIG if the zone is signed:

dig +dnssec isitdns.net @1.1.1.1 | grep RRSIG
isitdns.net.   300  IN  RRSIG  A 13 2 300 20260718170219 20260716150219 34505 isitdns.net. ...

The two timestamps are the signature validity window (expiration, then inception). Cloudflare re-signs continuously, so they will differ every time you look; the algorithm (13, ECDSA P-256) and key tag stay put between key rolls. The presence of an RRSIG record means signature data is in the response. Whether it validates depends on the resolver. Note: a validating resolver only sets ad in its reply when your query set the AD or DO bit (RFC 6840 §5.8). Recent dig sets the AD bit by default but older builds do not, so a missing ad flag is not proof that validation failed; the curl DoH method above is unambiguous.

nslookup cannot show the DNSSEC validation state. Use dot-doh or the curl DoH approach above.

PowerShell returns the RRSIG records in the answer set when -DnssecOk is on:

Resolve-DnsName isitdns.net -DnssecOk | Where-Object { $_.Type -eq 'RRSIG' }

Any RRSIG output means signature data is reaching you. PowerShell does not report a validation verdict, only the raw signature data.

delv is the BIND validator tool and gives a pass/fail verdict. It requires a BIND installation with crypto support (the macOS system delv at /usr/bin/delv ships without it). If you have a full BIND install:

delv cloudflare.com @1.1.1.1
; fully validated
delv dnssec-failed.org @1.1.1.1
;; resolution failed: failure

When you point delv at a validating resolver like 1.1.1.1, that resolver has already SERVFAILed the broken chain, so delv relays failure. When delv reaches the chain itself it names the specific fault, for example ;; broken trust chain. Either way it is a verdict.

delv has no nslookup equivalent. On Windows, install the BIND utilities to get delv, or use Resolve-DnsName -DnssecOk to surface RRSIG records.

9. The router/firewall in front of you is rewriting DNS#

Symptom: "I asked dig @1.1.1.1 directly, but I'm still getting an answer that 1.1.1.1 shouldn't know, internal hostnames resolve, or the answer matches my local network instead of the public Internet."

The cause: transparent DNS interception. Many home routers, firewalls (pfSense with pfBlockerNG, UniFi, prosumer security appliances), corporate gateways, and captive portals rewrite port-53 destination addresses so every query is answered from a local view, regardless of which resolver you targeted. The query never actually reaches 1.1.1.1; the local box hijacks it on the way out.

How to confirm:

Try a name only the public Internet knows:

dig @1.1.1.1 +short example.com

If you get back a private (RFC 1918) IP address, you have been intercepted. Confirm by running the same query from a host that sits outside the gateway:

ssh bastion-on-public-net 'dig @1.1.1.1 +short example.com'

Fixes: disable transparent DNS interception at the router (every vendor calls it something different), or query over an encrypted transport. Port-53 rewriting can only touch plaintext, so a DoT or DoH session rides past it to the resolver you actually asked for:

kdig +tls @1.1.1.1 example.isitdns.net

kdig is from the Knot DNS tools package. The +tls flag uses DNS-over-TLS on port 853, which the interception box cannot rewrite. example.isitdns.net resolves to 192.0.2.1 (A) and 2001:db8::1 (AAAA), both documentation addresses, so the answer is easy to verify and will never route to a real host.

nslookup cannot use DoT or DoH. Use curl with the DoH API or kdig +tls to bypass interception.

On consumer gear that is usually enough: a home router rewrites plaintext UDP/53, and switching to +tls reaches the far resolver it was hijacking. Enterprise kit is a different story. Corporate firewalls often block port 853 and known DoH endpoints outright, pin every device to an internal resolver, or terminate and inspect TLS, so the encrypted bypass may be limited or impossible. Either way it is a deliberate choice. A network can intercept plaintext and also block the outbound encrypted queries (drop port 853, blocklist the DoH resolvers), which leaves no bypass at all.

10. The "name resolves but the service is down" trap#

DNS works. The service doesn't. People still blame DNS. That is the meme. example.isitdns.net publishes both an A (192.0.2.1) and an AAAA (2001:db8::1), both documentation-only addresses that are not routable on the public Internet. That makes it a clean demonstration: DNS returns a valid answer, and then the connection goes nowhere.

nslookup example.isitdns.net 1.1.1.1
Non-authoritative answer:
Name:   example.isitdns.net
Address: 192.0.2.1

DNS did its job. Now try to reach it:

curl -v --max-time 5 http://example.isitdns.net
*   Trying [2001:db8::1]:80...
*   Trying 192.0.2.1:80...

On a dual-stack host, curl follows Happy Eyeballs: it attempts the AAAA address first and adds the A a moment later. Neither 192.0.2.1 nor 2001:db8::1 is routable, so curl hangs until --max-time 5 fires and exits with a connection-timeout error. (A host with no IPv6 route fails the first attempt instantly with Network is unreachable and then hangs on the A; same ending.) No bytes come back. The resolve step succeeded; the connect step never will.

That is the pattern to look for in real outages too. Trying <IP>... without a following Connected to means DNS is fine and the problem is at the service or the network path between you and it. DNS is innocent.


See also: dig-along, dig-flags, dnssec, anatomy-of-a-query