How to Troubleshoot DNS with dig: NXDOMAIN, SERVFAIL, DNSSEC (2026)

Oct 31, 2026 · 17 min · Ajay Kumar

This guide is for anyone with a name that will not resolve, resolves to the wrong thing, or works on a laptop but not in production. By the end you will be able to read every line of dig output, tell NXDOMAIN, NODATA, SERVFAIL and REFUSED apart, go straight to the authoritative servers, follow a delegation with +trace, prove whether DNSSEC is the problem, and account for caching and negative TTLs. Every output was captured on 24 October 2026 with the dig 9.10.6 that macOS still bundles and unbound-host from Unbound 1.22.0; flags are checked against the current BIND 9 manual pages, and the current stable BIND branch is 9.20 (9.20.30 on ISC's download server, dated 21 October 2026). You need a terminal with dig (from bind9-dnsutils, bind-utils or bind-tools, depending on distribution) and outbound UDP and TCP port 53. For the concepts behind all this, read the companion explainer, DNS explained for DevOps.

Step 1: Compare what your application sees with what dig sees

dig is not your application. It reads /etc/resolv.conf, ignores /etc/hosts, and does not apply the search list unless you pass +search (the BIND manual documents the resolv.conf and +search behaviour). Your application goes through the system resolver: glibc and nsswitch.conf on most Linux hosts, systemd-resolved where it is enabled, mDNSResponder on macOS. nslookup is a third path with its own resolver code. When dig and the application disagree, that disagreement is your first clue.

# Linux (glibc): what applications get, including /etc/hosts and nsswitch.conf order
getent ahosts api.example.com

# Linux with systemd-resolved: per-interface servers, cache and DNSSEC state
resolvectl status
resolvectl query api.example.com

# macOS: ask the system resolver, which may use per-domain or VPN resolvers
dscacheutil -q host -a name example.com
scutil --dns | head -30

# What dig sees: /etc/resolv.conf servers, no /etc/hosts, no search list
dig +short example.com

On my Mac the system resolver returned both address families:

$ dscacheutil -q host -a name example.com
name: example.com
ipv6_address: 2606:4700:10::6814:179a
ipv6_address: 2606:4700:10::ac42:93f3

name: example.com
ip_address: 172.66.147.243
ip_address: 104.20.23.154

If the application sees something dig does not, look at /etc/hosts, a VPN's split-DNS resolver (scutil --dns lists them), the search list and ndots (the classic Kubernetes case, covered in the CoreDNS troubleshooting guide), or a cache inside the runtime itself.

Step 2: Read dig output line by line

Run the full output once before reaching for +short.

$ dig +time=3 +tries=1 example.com A
; <<>> DiG 9.10.6 <<>> +time=3 +tries=1 example.com A
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 17392
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;example.com.			IN	A

;; ANSWER SECTION:
example.com.		38	IN	A	172.66.147.243
example.com.		38	IN	A	104.20.23.154

;; Query time: 11 msec
;; SERVER: 1.1.1.1#53(1.1.1.1)
;; WHEN: Sat Oct 24 15:45:09 IST 2026
;; MSG SIZE  rcvd: 72

What each part means:

  • status is the response code: NOERROR, NXDOMAIN, SERVFAIL or REFUSED for almost everything you will meet. Step 3 is about this line.
  • flags: qr marks a response, rd means you asked for recursion, ra means the server offers it, ad means the resolver validated the answer with DNSSEC, aa (absent here) means the answer came from an authoritative server, cd means you asked it not to validate, and tc means the answer was truncated and should be retried over TCP.
  • The counts show which sections are populated. ANSWER: 0 with NOERROR is NODATA, easy to misread as success.
  • OPT PSEUDOSECTION is EDNS: udp: 1232 is the buffer size, and Extended DNS Errors appear here (Step 6).
  • Answer lines are name, TTL in seconds, class, type, data. A TTL of 38 from a resolver is the remaining cache life, not the zone's configured value.
  • SERVER is who answered. If it is not the server you expected, stop there.

The flags I use most: +noall +answer for just the records, +short for scripts, +multiline for readable SOA and DNSKEY records, +norecurse against authoritative servers, @server to pick a server, and +short +identify to see which server answered:

$ dig +short +identify www.github.com
github.com. from server 1.1.1.1 in 38 ms.
20.207.73.82 from server 1.1.1.1 in 38 ms.

One 2026 caveat: the macOS dig predates the HTTPS record type. dig cloudflare.com HTTPS treats HTTPS as a second name, so it asks for cloudflare.com A and then HTTPS. A (NXDOMAIN from the root), and +ttlunits is rejected as an invalid option. Query TYPE65 instead and you get the raw bytes; a newer dig or a DNS JSON API decodes them:

# Old dig: raw RFC 3597 hex for the HTTPS record (type 65)
dig +noall +answer cloudflare.com TYPE65
# cloudflare.com.  300  IN  TYPE65  \# 61 0001000001000602683302683200040008681084E5...

# Decoded by Google Public DNS's JSON API
curl -s 'https://dns.google/resolve?name=cloudflare.com&type=HTTPS'
# ..."data":"1 . alpn=h3,h2 ipv4hint=104.16.132.229,104.16.133.229 ipv6hint=..."

Step 3: Tell NOERROR, NODATA, NXDOMAIN, SERVFAIL and REFUSED apart

The status line decides your next move.

DNS triage with dig: branch on the status line, then go to the step dig name @resolver read status in the HEADER line NOERROR + answer the data exists NOERROR, 0 answers NODATA: name exists NXDOMAIN name does not exist SERVFAIL resolver gave up App sees something else? Compare getent, resolvectl or dscacheutil (Step 1). Old value? Check the TTL countdown and flush caches (Step 7). Asked the wrong type, or a CNAME target is empty (Steps 3 and 8). Signed Cloudflare zones answer missing names this way too (RFC 9824). Ask each authoritative server, +norecurse (Step 4). Wrong delegation? Use +trace (Step 5). Just created it? Negative TTL from SOA (Step 7). Retry with +cd. Answer now? DNSSEC is bogus: read EDE (Step 6). Still SERVFAIL? Servers unreachable or lame: +trace (Step 5). REFUSED: you asked a server that is neither authoritative for the name nor willing to recurse for you. No status at all (connection timed out): network path, firewall, or blocked UDP/TCP port 53.
The triage path used in this guide. Status meanings follow RFC 2308 and RFC 9824; each box points to the step with the commands.

Here is each status as it actually arrives (output trimmed with +noall +comments +authority):

# NXDOMAIN: the name does not exist. The SOA in AUTHORITY sets how long that is cached.
$ dig nonexistent-zz9.google.com A +noall +comments +authority
;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN, id: 50118
google.com.  60  IN  SOA  ns1.google.com. dns-admin.google.com. 1005474929 900 900 1800 60

# NODATA: the name exists, but has no record of this type. Note ANSWER: 0.
$ dig www.google.com MX +noall +comments +authority
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 28817
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 1, ADDITIONAL: 1
google.com.  14  IN  SOA  ns1.google.com. dns-admin.google.com. 1005474929 900 900 1800 60

# REFUSED: an authoritative server asked about a zone it does not serve
$ dig @hera.ns.cloudflare.com google.com A +norecurse
;; ->>HEADER<<- opcode: QUERY, status: REFUSED, id: 6970
;; flags: qr; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1

SERVFAIL gets its own step below because the cause is usually DNSSEC or unreachable servers. RFC 2308 defines NODATA as NOERROR with no relevant answers, so your tooling has to infer it from the counts. Note one 2025 change: RFC 9824 compact denial lets signed zones answer a nonexistent name with NOERROR and an NSEC record listing the NXNAME type (128). Under example.com, which Cloudflare serves, a made-up name returns NOERROR with NSEC \000.nonexistent-zz9.example.com. RRSIG NSEC TYPE128 instead of NXDOMAIN. If a check depends on NXDOMAIN, test it against your provider.

Step 4: Query the authoritative servers directly

Asking the authoritative servers directly removes every cache from the picture. Find who the parent zone says is authoritative, then ask each server with recursion off and look for the aa flag.

# 1. The delegation as published by the parent (.com) zone
dig @a.gtld-servers.net example.com NS +norecurse +noall +authority
# example.com.  172800  IN  NS  hera.ns.cloudflare.com.
# example.com.  172800  IN  NS  elliott.ns.cloudflare.com.

# 2. The NS set the zone itself publishes (should match the parent)
dig @hera.ns.cloudflare.com example.com NS +norecurse +noall +answer
# example.com.  86400  IN  NS  elliott.ns.cloudflare.com.
# example.com.  86400  IN  NS  hera.ns.cloudflare.com.

# 3. The record itself, straight from the source: expect flags "qr aa"
dig @hera.ns.cloudflare.com example.com A +norecurse +noall +answer
# example.com.  300  IN  A  172.66.147.243
# example.com.  300  IN  A  104.20.23.154

# 4. Every authoritative server's SOA serial in one go
dig +nssearch example.com
# SOA elliott.ns.cloudflare.com. dns.cloudflare.com. 2418004112 10000 2400 604800 1800 from server 108.162.192.162 in 18 ms.
# SOA elliott.ns.cloudflare.com. dns.cloudflare.com. 2418004112 10000 2400 604800 1800 from server 173.245.58.162 in 26 ms.

Check three things. If the parent's NS list and the zone's NS list differ, you have a stale or partial delegation, usually left over from a provider move. If a listed server returns REFUSED or an answer without aa, it is a lame delegation: the parent points at a server that does not serve the zone. If +nssearch shows different serials, a secondary has not picked up the latest zone. Note the TTLs too: the parent hands out the delegation for 172,800 seconds (48 hours), which is why nameserver changes are the slowest DNS change you can make.

Step 5: Follow the delegation with +trace

+trace does what a recursive resolver does, visibly: it starts at the root and prints every referral. Per the BIND manual it disables recursion and turns on +dnssec, which is why RRSIG lines appear in the output. Your configured resolver is used only for the initial root NS query.

$ dig +trace example.com A        # RRSIG lines and 24 of the 28 NS lines removed
.             513462  IN  NS  a.root-servers.net.
;; Received 525 bytes from 1.1.1.1#53(1.1.1.1) in 16 ms
com.          172800  IN  NS  a.gtld-servers.net.
com.          86400   IN  DS  19718 13 2 8ACBB0CD28F41250A80A491389424D341522D946B0DA0C0291F2D3D7 71D7805A
;; Received 1171 bytes from 170.247.170.2#53(b.root-servers.net) in 78 ms
example.com.  172800  IN  NS  hera.ns.cloudflare.com.
example.com.  172800  IN  NS  elliott.ns.cloudflare.com.
example.com.  86400   IN  DS  2371 13 2 C988EC423E3880EB8DD8A46FE06CA230EE23F35B578D64E78B29C3E1 C83D245A
;; Received 506 bytes from 192.54.112.30#53(h.gtld-servers.net) in 147 ms
example.com.  300     IN  A   172.66.147.243
example.com.  300     IN  A   104.20.23.154
;; Received 179 bytes from 108.162.195.228#53(elliott.ns.cloudflare.com) in 11 ms

The root refers you to .com, .com refers you to Cloudflare's servers (and publishes a DS record, so the zone is signed), and Cloudflare answers. Where the trace stops is where the problem is. A timeout at the last hop means the authoritative servers are unreachable; NXDOMAIN at the .com hop means the domain is not delegated at all (expired, or never registered). If +trace works but your resolver still fails, the problem is the resolver: a cached negative answer, a DNSSEC failure, or its own network path.

Step 6: Debug DNSSEC failures with +cd, EDE codes and a validator

A SERVFAIL that turns into an answer when you set the CD (checking disabled) bit is a DNSSEC failure: the data exists, but the resolver will not vouch for it. dnssec-failed.org is a deliberately broken test domain, and it shows the pattern:

$ dig dnssec-failed.org A
;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 32567
;; flags: qr rd ra; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
; OPT=15: 00 09 6e 6f 20 53 45 50 ... ("..no SEP matching the DS found for dnssec-failed.org.")

$ dig +cd +noall +comments +answer dnssec-failed.org A
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 24880
;; flags: qr rd ra cd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
dnssec-failed.org.  300  IN  A  96.99.227.255

OPT=15 is an Extended DNS Error. The first two bytes, 00 09, are the INFO-CODE: 9, "DNSKEY Missing" in the IANA registry. Old dig prints it raw; current versions print a line like ; EDE: 9 (DNSKEY Missing): (no SEP matching the DS found for ...), as Cloudflare's EDE reference shows. The codes you will meet most: 6 (DNSSEC Bogus), 7 (Signature Expired), 9 (DNSKEY Missing, usually a stale DS at the registrar), 10 (RRSIGs Missing), 22 (No Reachable Authority) and 23 (Network Error).

Next, compare the parent's DS with the zone's keys. The key tag and algorithm in the DS must match a DNSKEY with flags 257:

dig example.com DS +short
# 2371 13 2 C988EC423E3880EB8DD8A46FE06CA230EE23F35B578D64E78B29C3E1 C83D245A

dig example.com DNSKEY +multiline +noall +answer | grep "key id"
#     ) ; ZSK; alg = ECDSAP256SHA256 ; key id = 36315
#     ) ; ZSK; alg = ECDSAP256SHA256 ; key id = 9776
#     ) ; ZSK; alg = ECDSAP256SHA256 ; key id = 34505
#     ) ; KSK; alg = ECDSAP256SHA256 ; key id = 2371    <- matches DS 2371, algorithm 13

For an independent verdict, use a validator of your own. delv is BIND's, but on macOS the bundled one is unusable; it printed ;; none:29: no crypto support and delv: No trusted keys were loaded for every query. On Linux, a current delv (BIND 9.18 or 9.20) validates against its built-in root anchor, and +vtrace logs each validation step. On a Mac, Unbound's unbound-host does the job with a trust anchor file you fetch into a working directory:

# Unbound 1.22.0 (Homebrew). Creates or updates the root trust anchor in ./root.key.
unbound-anchor -a ./root.key
grep -o "id = [0-9]* (ksk)" ./root.key
# id = 38696 (ksk)     <- KSK-2024, signing the root since 11 October 2026
# id = 20326 (ksk)

unbound-host -v -f ./root.key -t A dnssec-failed.org
# dnssec-failed.org has address 96.99.227.255 (BOGUS (security failure))
# validation failure <dnssec-failed.org. A IN>: no keys have a DS with algorithm
#   ECDSAP256SHA256 from 68.87.85.132 for key dnssec-failed.org. while building chain of trust

unbound-host -v -f ./root.key -t A example.com
# example.com has address 172.66.147.243 (secure)

That key tag check matters this month. ICANN's rollover notice says resolvers without key tag 38696 fail all resolution after 11 October 2026, so if every validated name suddenly returns SERVFAIL from one resolver, check its trust anchor file (bind.keys, root.key or root.keys) first.

The usual causes are a DS left at the registrar after a provider move (EDE 9), a signer that stopped re-signing (EDE 7), or an algorithm the resolver does not support (EDE 1). To move a signed zone without multi-signer support, remove the DS at the registrar, wait out the DS TTL (86,400 seconds for example.com in the trace above) plus margin, move the zone, then sign it and publish the new DS.

Step 7: Account for caching and negative TTLs

Once the authoritative answer is right, everything else is caches. Compare the TTL at the source with your resolver's:

$ dig @hera.ns.cloudflare.com example.com A +norecurse +noall +answer
example.com.  300  IN  A  172.66.147.243          # configured TTL at the source
$ dig example.com A +noall +answer
example.com.  225  IN  A  172.66.147.243          # 1.1.1.1: 225 seconds of cache left

Do not expect a smooth countdown from a public resolver. Five consecutive queries to 1.1.1.1 returned TTLs of 141, 124, 70, 152 and 139, because one anycast address fronts many caches; five to 8.8.8.8 returned 300 each time. Wait out the TTL configured at the source, not the one a resolver last showed you.

Negative answers are cached too. Per RFC 2308 the negative TTL is the lower of the SOA record's TTL and its MINIMUM field (the last number). For google.com above that is 60 seconds; for example.com it is 1,800. If someone queried a name before you created it, that NXDOMAIN can outlive your change by that long. Resolvers must also cache outright failures, between one second and five minutes under RFC 9520.

Flushing, from the client outward:

# macOS
sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder
# Linux with systemd-resolved
resolvectl flush-caches
# Windows (PowerShell or cmd)
ipconfig /flushdns
# Your own resolvers
unbound-control flush_zone example.com    # Unbound: names at and below example.com
rndc flushname example.com                # BIND: one name
# Public resolvers: Google's form at https://developers.google.com/speed/public-dns/cache
# and Cloudflare's at https://one.one.one.one/purge-cache/

Google's Flush Cache page advises flushing the parent domain before subdomains after a registrar or DNS host change; Cloudflare's purge tool takes a name and a record type.

Step 8: Fix CNAME at the apex and other record conflicts

RFC 1034 says that if a CNAME is present at a node, no other data should be present. The apex always has SOA and NS records, so a CNAME there is invalid, and providers reject it. Route 53 fails the change with InvalidChangeBatch and "RRSet of type CNAME with DNS name example.com. is not permitted at apex in zone example.com", as AWS's knowledge centre describes, and BIND refuses to load a zone with the error "CNAME and other data". The rule also bites when someone adds a TXT verification record beside an existing CNAME.

On Route 53, use an alias record. CloudFront's alias hosted zone ID is always Z2FDTNDATAQYW2, per the CloudFormation Route 53 snippets:

{
  "Comment": "Apex alias to CloudFront instead of an invalid CNAME; repeat with Type AAAA for IPv6",
  "Changes": [
    {
      "Action": "UPSERT",
      "ResourceRecordSet": {
        "Name": "example.com.",
        "Type": "A",
        "AliasTarget": {
          "HostedZoneId": "Z2FDTNDATAQYW2",
          "DNSName": "d111111abcdef8.cloudfront.net.",
          "EvaluateTargetHealth": false
        }
      }
    }
  ]
}
# AWS CLI v2; replace the hosted zone ID with your own zone's ID
aws route53 change-resource-record-sets \
  --hosted-zone-id Z0123456789EXAMPLE \
  --change-batch file://apex-alias.json

Route 53 alias records can target only AWS resources or records in the same zone, and you cannot set their TTL, per the alias guide. Elsewhere, use flattening; Cloudflare's CNAME flattening returns the target's IP and warns that a dangling target produces an empty NODATA answer. The standards-based option is an HTTPS record in AliasMode, example.com. 3600 IN HTTPS 0 svc.example.net. from RFC 9460, but only clients that query HTTPS records will follow it, so keep A and AAAA records for everyone else. If certificate issuance fails at this point, remember that CAA lookups follow CNAMEs to the target's zone, as Let's Encrypt documents, and the cert-manager guide covers the ACME side.

Troubleshooting

Symptom Likely cause Fix
dig resolves, the application does not /etc/hosts, VPN split DNS, search list or runtime cache Compare with getent ahosts, resolvectl query or dscacheutil (Step 1)
NOERROR with ANSWER: 0 NODATA: wrong record type, or CNAME target missing Query the type you need; follow the CNAME chain (Steps 3, 8)
Missing names in a signed Cloudflare zone never return NXDOMAIN RFC 9824 compact denial returns NOERROR plus NSEC TYPE128 Test for NXNAME in the NSEC bitmap, not only the rcode (Step 3)
New record shows NXDOMAIN for a while Negative caching from an earlier lookup Wait min(SOA TTL, SOA MINIMUM) or flush resolvers (Step 7)
SERVFAIL, but +cd returns an answer DNSSEC bogus: stale DS, expired RRSIG, bad key Read the EDE code; compare DS and DNSKEY key tags (Step 6)
Every signed name SERVFAILs on one resolver since 11 October 2026 Resolver lacks root KSK-2024 (key tag 38696) Update the trust anchor file or resolver package (Step 6)
Authoritative server answers REFUSED or without aa Lame delegation after a provider move Make the registrar's NS list match the zone's NS records (Step 4)
Different answers from different authoritative servers Secondary not updated; serials differ in +nssearch Fix zone transfer or provider sync, bump the serial (Step 4)
dig name HTTPS returns A records plus an NXDOMAIN for HTTPS., or TYPE65 hex dig older than HTTPS record support (macOS 9.10.6) Query TYPE65 and decode, or use a current BIND dig (Step 2)
"not permitted at apex" or "CNAME and other data" CNAME at the zone apex or beside other records Use alias, flattening, A/AAAA or HTTPS AliasMode (Step 8)

FAQ

What is the difference between dig and nslookup?

Both send DNS queries, but dig prints the full response (flags, TTLs, authority section, EDNS options) and scripts cleanly with +short, while nslookup summarises. Neither shows exactly what your application sees; use getent, resolvectl or dscacheutil for that.

What does SERVFAIL mean in DNS?

The recursive resolver could not produce a trustworthy answer. The two big causes are DNSSEC validation failure (test with dig +cd, then read the EDE code) and authoritative servers that are unreachable or lame (test with +trace and +norecurse). Resolvers cache SERVFAIL for up to five minutes, so retest after fixing.

How do I check DNS propagation?

Query each authoritative server with +norecurse to confirm the change is live at the source, then query the resolvers your users actually use and compare TTLs. Anything still old will expire within the previous TTL, or within the parent's delegation TTL (48 hours for .com) if you changed nameservers.

Why does dig show TYPE65 instead of HTTPS?

Your dig predates the HTTPS record defined in RFC 9460, so it prints the record in generic hex form. The 9.10.6 build bundled with macOS does this. Install a current BIND build, or decode through a DNS JSON API such as dns.google/resolve.

In production I check three things before anything else: what the application's resolver returns compared with dig, what the authoritative servers say with +norecurse, and whether +cd turns a SERVFAIL into an answer. Those three settle most DNS incidents quickly and stop me flushing caches at random. For the rest, the official references are the BIND 9 manual pages for dig and delv, RFC 2308 for negative caching, the IANA DNS parameters registry for record types and EDE codes, and Cloudflare's EDE reference.

Related: How to Troubleshoot Kubernetes DNS and CoreDNS: Step by Step (2026), How to Build Multi-Region Failover with Route 53 and Cloud DNS (2026) and How to Set Up cert-manager with Let's Encrypt and Fix Errors (2026).

I'm Ajay Kumar — I build and operate PandaStack, an open-source Firecracker microVM cloud for AI agents, and write about the infrastructure and tooling around it.

Need this kind of infrastructure work? See what I do or email hello@ajayk.sh.


Related