Diagnostic Core Records
Missing A or AAAA record causes NXDOMAIN
You visit a subdomain, or your app tries to reach it, and nothing answers. Not a timeout, not a wrong page, just NXDOMAIN, the DNS way of saying "this name does not exist." Here is why that happens, how to tell it apart from a similar-looking but different problem, and how to add the missing record so the name resolves again.
NXDOMAIN on a hostname means the zone has no record of any kind for that exact name, not an A record, not an AAAA record, not even a CNAME. This usually happens when a subdomain got used in an app or a link before anyone created its DNS record, or a record was deleted during cleanup and nobody noticed. The fix is to add the missing A, AAAA, or CNAME record in the authoritative zone. Full commands, records, and a small watcher script are below.
The problem in plain words
Every DNS answer carries a status code, called an rcode. Two of them look similar but mean very different things. NXDOMAIN means "this name does not exist, not for anything." NOERROR with an empty answer, usually called NODATA, means "this name exists, just not for the record type you asked about." Mixing these up is one of the most common DNS misreadings there is.
When a subdomain like app.example.com has never had any record created for it, or its last record was deleted without a replacement, the authoritative nameserver has nothing to say about that name at all. So it correctly answers NXDOMAIN. RFC 8020 makes this explicit: NXDOMAIN is a statement about the whole name and everything underneath it, not just the one record type you happened to ask for.
Why it happens
- A developer wired up a subdomain in an app, a link, or a marketing email before the DNS record for it was ever created.
- A record was deleted during zone cleanup, or during a migration to a new DNS provider, and the host it pointed to was never given a replacement record.
- A typo in the record name means the record exists, just not at the exact hostname being queried, so the name you are actually asking about still has nothing.
- A CNAME chain got broken partway, leaving the final hostname in the chain with no record to answer with.
It is easy to misread this as "the DNS is down" or "the record type is wrong." It is neither. The zone has been asked about a name and has nothing on file for it, at all.
NXDOMAIN and NODATA look the same in a browser (both fail), but they are opposite diagnoses. NXDOMAIN (status: NXDOMAIN) means the name itself does not exist. NODATA (status: NOERROR with an empty answer section) means the name exists but has no record of the type you asked for, for example a name with only an MX record when you queried for A. Per RFC 8020, confirming which one you have from the header, not from guessing, is the first step.
The fix, as a flow
Confirm the authoritative nameservers themselves return NXDOMAIN for the exact name, decide whether the target should be an IP address or another hostname, then add the matching record type in the zone and verify it against the authoritative server before checking public resolvers.
How to fix it
Confirm the name really has nothing, not just the wrong type
Query both A and AAAA for the hostname, and look at the header, not just the answer. A missing-record NXDOMAIN shows status: NXDOMAIN with an empty ANSWER section. If instead you see status: NOERROR with an empty ANSWER section, that is NODATA, a different problem, because the name does exist for some other record type.
dig A app.example.com +noall +comment
dig AAAA app.example.com +noall +comment
dig app.example.com CNAME
Ask the authoritative nameserver directly, bypassing caches
A resolver or a cache can hold a stale negative answer. Skip that layer and ask the zone's own nameserver. If the authoritative server itself returns NXDOMAIN, the zone truly has no record for that name, which is the exact case this note describes. Also trace the full delegation chain to see exactly where the answer comes from.
dig @ns1.example.com app.example.com A
dig +trace app.example.com A
Add the missing record in the authoritative zone
If the name should point straight at a server, add an A record for an IPv4 target or an AAAA record for an IPv6 target. If the name should point at another hostname instead, such as a CDN or a load balancer, add a CNAME instead. A CNAME cannot share a name with any other record, so pick one or the other.
app.example.com 300 IN A 203.0.113.10
app.example.com 300 IN AAAA 2001:db8::10
; or, if it should point to another hostname instead:
app.example.com 300 IN CNAME target.cdnprovider.net.
Create it in Cloudflare, by dashboard or by API
In Cloudflare's dashboard, go to DNS, Records, Add record, choose type A, AAAA, or CNAME, enter the name and the target, set the TTL and proxy status, and save. To do the same thing through the API, send a create request against the zone's DNS records endpoint.
curl -X POST "https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records" \
-H "Authorization: Bearer {CLOUDFLARE_API_TOKEN}" \
-H "Content-Type: application/json" \
--data '{"type":"A","name":"app.example.com","content":"203.0.113.10","ttl":300,"proxied":false}'
How to check it worked
Re-query the name and look for status: NOERROR with an answer this time. Check the authoritative server first, since that confirms the zone itself is fixed rather than a cache. Then check a couple of public resolvers to see it has propagated. If a public resolver still shows the old NXDOMAIN, that is the negative cache from the zone's SOA still holding, and it clears once that TTL runs out. Finish with a real connection check.
dig A app.example.com +noall +answer
dig @ns1.example.com app.example.com A
dig @1.1.1.1 app.example.com A
dig @8.8.8.8 app.example.com A
curl -Iv https://app.example.com
# a good answer looks like:
# ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 9182
# app.example.com. 300 IN A 203.0.113.10
The full code
Here is a small script that queries a hostname's authoritative nameservers directly, classifies the answer as a true missing-record NXDOMAIN versus a NODATA wrong-type answer, and, when a name expected to be live comes back NXDOMAIN, creates the missing record through the Cloudflare API. It stays in dry run by default so it reports before it writes.
View this code on GitHub Full runnable folder with tests in the dns-fixes repo.
"""Detect a missing A/AAAA record (true NXDOMAIN) and repair it via Cloudflare.
Safe to run on a schedule. Stays in dry run until DRY_RUN=false.
"""
import os
import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("missing_address_record")
def classify_missing_record(rcode: str, answer_count: int, expected_name_exists: bool) -> str:
"""Pure decision function. No network, no I/O."""
if rcode == "NXDOMAIN" and answer_count == 0 and expected_name_exists:
return "missing_record_nxdomain"
if rcode == "NOERROR" and answer_count == 0:
return "nodata_wrong_type"
if answer_count > 0:
return "ok"
return "unexpected"
def run():
# Imported lazily so the pure function above can be tested with no
# network libraries installed at all.
import dns.resolver
import dns.rdatatype
import requests
domain = os.environ["DNS_DOMAIN"]
record_type = os.environ.get("RECORD_TYPE", "A")
target = os.environ.get("RECORD_TARGET", "203.0.113.10")
ttl = int(os.environ.get("RECORD_TTL", "300"))
dry_run = os.environ.get("DRY_RUN", "true").lower() == "true"
zone_id = os.environ["CLOUDFLARE_ZONE_ID"]
api_token = os.environ["CLOUDFLARE_API_TOKEN"]
# Find the authoritative nameservers for the zone, then query them
# directly so the answer cannot be a stale cache.
ns_answer = dns.resolver.resolve(domain, "NS")
nameservers = [str(r.target).rstrip(".") for r in ns_answer]
ns_ip = dns.resolver.resolve(nameservers[0], "A")[0].address
resolver = dns.resolver.Resolver()
resolver.nameservers = [ns_ip]
try:
answer = resolver.resolve(domain, record_type)
rcode = "NOERROR"
answer_count = len(answer)
except dns.resolver.NXDOMAIN:
rcode = "NXDOMAIN"
answer_count = 0
except dns.resolver.NoAnswer:
rcode = "NOERROR"
answer_count = 0
outcome = classify_missing_record(rcode, answer_count, expected_name_exists=True)
log.info("Name %s classified as %s (rcode=%s, answers=%d)", domain, outcome, rcode, answer_count)
if outcome != "missing_record_nxdomain":
log.info("Nothing to repair.")
return
log.info("Missing %s record for %s. %s create it pointing to %s.",
record_type, domain, "Would" if dry_run else "Will", target)
if dry_run:
return
resp = requests.post(
f"https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records",
headers={"Authorization": f"Bearer {api_token}", "Content-Type": "application/json"},
json={"type": record_type, "name": domain, "content": target, "ttl": ttl},
timeout=30,
)
resp.raise_for_status()
log.info("Created %s record for %s -> %s", record_type, domain, target)
if __name__ == "__main__":
run()
/**
* Detect a missing A/AAAA record (true NXDOMAIN) and repair it via Cloudflare.
* Safe to run on a schedule. Stays in dry run until DRY_RUN=false.
*/
import { pathToFileURL } from "node:url";
export function classifyMissingRecord(rcode, answerCount, expectedNameExists) {
// Pure decision function. No network, no I/O.
if (rcode === "NXDOMAIN" && answerCount === 0 && expectedNameExists) {
return "missing_record_nxdomain";
}
if (rcode === "NOERROR" && answerCount === 0) {
return "nodata_wrong_type";
}
if (answerCount > 0) {
return "ok";
}
return "unexpected";
}
export async function run() {
// Imported lazily so the pure function above can be tested with no
// network modules touched at all.
const dns = await import("node:dns");
const resolvePromises = dns.promises;
const domain = process.env.DNS_DOMAIN;
const recordType = process.env.RECORD_TYPE || "A";
const target = process.env.RECORD_TARGET || "203.0.113.10";
const ttl = Number(process.env.RECORD_TTL || 300);
const dryRun = (process.env.DRY_RUN || "true").toLowerCase() === "true";
const zoneId = process.env.CLOUDFLARE_ZONE_ID;
const apiToken = process.env.CLOUDFLARE_API_TOKEN;
// Find the authoritative nameservers for the zone, then query them
// directly so the answer cannot be a stale cache.
const nameservers = await resolvePromises.resolveNs(domain);
const nsIp = (await resolvePromises.resolve4(nameservers[0]))[0];
const resolver = new dns.promises.Resolver();
resolver.setServers([nsIp]);
let rcode = "NOERROR";
let answerCount = 0;
try {
const answers = recordType === "AAAA"
? await resolver.resolve6(domain)
: await resolver.resolve4(domain);
answerCount = answers.length;
} catch (err) {
if (err.code === "ENOTFOUND") {
rcode = "NXDOMAIN";
} else if (err.code === "ENODATA") {
rcode = "NOERROR";
answerCount = 0;
} else {
throw err;
}
}
const outcome = classifyMissingRecord(rcode, answerCount, true);
console.log(`Name ${domain} classified as ${outcome} (rcode=${rcode}, answers=${answerCount})`);
if (outcome !== "missing_record_nxdomain") {
console.log("Nothing to repair.");
return;
}
console.log(`Missing ${recordType} record for ${domain}. ${dryRun ? "Would" : "Will"} create it pointing to ${target}.`);
if (dryRun) return;
const res = await fetch(`https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records`, {
method: "POST",
headers: { Authorization: `Bearer ${apiToken}`, "Content-Type": "application/json" },
body: JSON.stringify({ type: recordType, name: domain, content: target, ttl }),
});
if (!res.ok) throw new Error(`Cloudflare API returned ${res.status}`);
console.log(`Created ${recordType} record for ${domain} -> ${target}`);
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
run().catch((err) => { console.error(err); process.exit(1); });
}
Add a test
The classifier is the part worth testing on its own, because it decides whether the script thinks a record is really missing or just the wrong type. It takes plain strings, an int, and a bool, so the test needs no network and no DNS library at all.
from missing_address_record import classify_missing_record
def test_missing_record_nxdomain():
assert classify_missing_record("NXDOMAIN", 0, True) == "missing_record_nxdomain"
def test_nodata_wrong_type():
assert classify_missing_record("NOERROR", 0, True) == "nodata_wrong_type"
def test_ok_when_answers_present():
assert classify_missing_record("NOERROR", 1, True) == "ok"
def test_nxdomain_not_expected_is_unexpected():
assert classify_missing_record("NXDOMAIN", 0, False) == "unexpected"
import { test } from "node:test";
import assert from "node:assert/strict";
import { classifyMissingRecord } from "./missing-address-record.js";
test("missing record nxdomain", () => {
assert.equal(classifyMissingRecord("NXDOMAIN", 0, true), "missing_record_nxdomain");
});
test("nodata wrong type", () => {
assert.equal(classifyMissingRecord("NOERROR", 0, true), "nodata_wrong_type");
});
test("ok when answers present", () => {
assert.equal(classifyMissingRecord("NOERROR", 1, true), "ok");
});
test("nxdomain not expected is unexpected", () => {
assert.equal(classifyMissingRecord("NXDOMAIN", 0, false), "unexpected");
});
Case studies
The mobile app that pointed at a subdomain nobody created
A team shipped a mobile app update pointing its API client at api-v2.example.com. The record for it was on a list for the infra team to create the same week, but the app release went out first. Every request from the new app version failed with a DNS lookup error, which support first reported as "the app is down."
A quick dig A api-v2.example.com showed status: NXDOMAIN with nothing in ANSWER. Adding the A record pointing at the existing load balancer IP fixed it within the TTL window, no app release needed.
The record that got left behind during a DNS move
A store migrated its DNS from one provider to another. The migration script copied over the main records but skipped a rarely used staging subdomain because it was not in the export the team pulled. Nobody noticed until a scheduled job that hit that subdomain started failing every night.
Querying the new authoritative nameservers directly confirmed the zone had genuinely lost the record, not just a caching delay. Re-adding the CNAME the old zone had, pointed at the same staging host, resolved it.
Once the record exists, dig A app.example.com against the authoritative nameserver returns status: NOERROR with the record in the ANSWER section, public resolvers agree once the negative cache clears, and curl -Iv https://app.example.com connects to the expected service. A watcher script like the one above catches a repeat of this the next time a subdomain gets referenced before its record exists.
FAQ
What does NXDOMAIN actually mean?
NXDOMAIN means the name you asked about does not exist anywhere in the zone, for any record type. It is different from NODATA, which means the name exists but has no record of the type you asked for. If a hostname has no A, AAAA, or CNAME record at all, the authoritative server returns NXDOMAIN.
How do I tell NXDOMAIN apart from NODATA?
Look at the header from dig. NXDOMAIN shows status: NXDOMAIN with an empty ANSWER section. NODATA shows status: NOERROR with an empty ANSWER section. The rcode is the whole difference: NXDOMAIN says the name does not exist, NOERROR with no answer says the name exists but not for that record type.
Why is my new subdomain still returning NXDOMAIN after I added the record?
You are likely hitting a cached negative answer. Resolvers cache NXDOMAIN for the negative-caching TTL set in the zone's SOA record. Query the authoritative nameserver directly with dig @ns1.example.com to confirm the zone itself now serves the record, then wait out the negative cache on public resolvers.
Related field notes
Citations
On the problem:
- RFC 8020: NXDOMAIN, there really is nothing underneath. rfc-editor.org/rfc/rfc8020.html
- RFC 2308: Negative caching of DNS queries (DNS NCACHE). rfc-editor.org/rfc/rfc2308.html
- Cloudflare Learning: what is a DNS A record. cloudflare.com/learning/dns/dns-records/dns-a-record
On the solution:
- Cloudflare DNS docs: create DNS records. developers.cloudflare.com/dns/manage-dns-records/how-to/create-dns-records
- Cloudflare API: DNS records, create. developers.cloudflare.com/api/resources/dns/subresources/records/methods/create
- AWS Route 53: working with records. docs.aws.amazon.com/Route53/latest/DeveloperGuide/rrsets-working-with.html
Stuck on a tricky one?
If you have a DNS, email deliverability, or certificate problem you would rather hand off, this is the kind of work I do. Message me and we can work through it together.
Did this fix your NXDOMAIN?
If this saved you a confusing afternoon of DNS debugging, you can buy me a coffee. It is the best way to keep these field notes free and growing.
Buy me a coffee on Ko-fi