Reconciler Core Records
www and apex configured inconsistently
Someone opens the site at www.example.com and it loads fine. Someone else, or a script, or an old link, hits example.com without the www and gets a blank error, a parked page, or a completely different site. Both names are supposed to serve the same thing. When only one of them was ever set up, or the two were pointed at different places, the two names quietly drift apart and only some of your visitors notice.
Most sites need two DNS records to answer under both names: an A/AAAA record (or a flattened ALIAS) at the bare apex, and a CNAME at www pointing at the same host. Because a CNAME cannot legally sit next to the SOA and NS records required at the apex, people often configure one variant and forget the other, or point the two at different backends. Add the missing record so both names resolve to the same host, then add a 301 redirect so one is canonical. Full code, tests, and a dry run guard are below.
The problem in plain words
A domain like example.com is really two different names as far as DNS is concerned. There is the bare apex, example.com itself, and there is www.example.com, a completely separate hostname that happens to share the same brand. Nothing in DNS makes them equal automatically. Each one needs its own record, pointing at the same place, or they will not agree.
The apex almost always gets an A or AAAA record, because a CNAME cannot live there without breaking the SOA and NS records DNS requires at that name. The www name is usually simpler, a plain CNAME to your host or CDN. Because the two records live in different rows of the DNS panel and use different record types, it is easy to set up one, ship the site, and never circle back to the other. Weeks or months later, someone shares the bare domain in an email or a business card, and it fails while www keeps working fine, or the reverse.
Why it happens
- A CNAME cannot legally sit at the zone apex alongside the required SOA and NS records, per RFC 1034/1035, so the apex needs a different kind of record than www, and that extra step gets skipped.
- A quick-start guide says "add a CNAME for www" and stops there, leaving the reader to assume the apex is handled automatically. It is not.
- The site launched on www only, and a later rebrand or campaign started advertising the bare domain without anyone checking that it actually resolved.
- The two records exist but were set up at different times by different people, pointing at different hosting accounts, different CDNs, or a stale server that was since retired.
This is a direct consequence of a rule that has been part of DNS since RFC 1034, and Cloudflare's own docs describe the same apex restriction and the flattening feature built to work around it.
www and the apex are two independent DNS names that happen to share a domain. DNS never keeps them in sync for you. If you want both to work, you must configure both, on purpose, and then decide which one is canonical with a redirect. Skipping either step is how one name quietly stops working while the other keeps serving traffic.
The fix, as a flow
Pick one name as canonical, say www.example.com. Make sure the apex has a working A/AAAA record (or a flattened ALIAS) pointing at the same host as www. Make sure www has a working CNAME. Then add a redirect at the host or CDN so the non-canonical name always forwards to the canonical one, instead of silently serving different or broken content.
How to fix it
Resolve both names and compare
Check the apex and www side by side. A missing side returns nothing (NXDOMAIN). A mismatched side returns a real answer that just does not match the other name's IPs.
dig +short A example.com
dig +short AAAA example.com
dig +short CNAME www.example.com
dig +short A www.example.com
dig +short NS example.com
dig @<ns1> example.com A
dig @<ns1> www.example.com A
Compare the HTTP behavior too
DNS can agree while the servers still disagree, or one name can time out entirely. Check status codes and redirect targets on both.
curl -sI https://example.com
curl -sI https://www.example.com
# bad: one returns HTTP/2 200 and the other times out, 404s, or shows a parked page
# bad: both return 200 but with different content-length and no redirect between them
Add the missing apex record
If the apex is the side that fails, add A records (and AAAA if your host offers it) pointing at your host's edge IPs. If your provider supports ALIAS, ANAME, or CNAME flattening, you can point the apex at the same target as www instead.
# Example: GitHub Pages edge IPs at the apex
Type: A Name: @ Value: 185.199.108.153 TTL: 300
Type: A Name: @ Value: 185.199.109.153 TTL: 300
Type: A Name: @ Value: 185.199.110.153 TTL: 300
Type: A Name: @ Value: 185.199.111.153 TTL: 300
# Or, on a provider with apex aliasing (Cloudflare, Route 53, DNSimple):
Type: ALIAS/ANAME Name: @ Value: www.example.com TTL: Auto
Add the missing www record
If www is the side that fails, add a plain CNAME pointing at the apex or at your host's target hostname.
Type: CNAME Name: www Value: example.com TTL: 300
# or, pointing straight at the host:
Type: CNAME Name: www Value: yourhost.github.io TTL: 300
Type: CNAME Name: www Value: your-app.vercel-dns.com TTL: 300
Enforce one canonical direction with a redirect
Even after both names resolve, set up a 301 redirect at the host or CDN level so one name always forwards to the other. This keeps the two from silently serving different content in the future.
# Cloudflare Bulk Redirect / Page Rule example
Match: https://example.com/*
Action: 301 -> https://www.example.com/$1
# Reverse direction if the apex is canonical instead:
Match: https://www.example.com/*
Action: 301 -> https://example.com/$1
Set both records' TTL to 300 seconds before making the change so propagation is fast, then raise it back to a normal value like 3600 once you have confirmed everything matches.
How to check it worked
Re-run the same checks and confirm both names agree, from multiple public resolvers, and that the redirect lands on one canonical URL.
dig +short A example.com
dig +short A www.example.com
# expect: the same IP set on both, or the www CNAME chain resolving to the same edge
curl -sI https://example.com
curl -sI https://www.example.com
# expect: both HTTP/2 200, or a clean 301 from the non-canonical straight to the canonical one
curl -s https://example.com | md5sum
curl -s https://www.example.com | md5sum
# expect: matching bodies once redirects are followed
dig @8.8.8.8 www.example.com
dig @1.1.1.1 www.example.com
# expect: both public resolvers agree, confirming propagation
The full code
Here is a small script in each language that resolves both names, flags the mismatch, and, when told to, repairs it through the Cloudflare API. It stays in dry run by default so it only reports what it would change.
View this code on GitHub Full runnable folder with tests in the dns-fixes repo.
"""Detect a www/apex DNS mismatch and optionally repair it via Cloudflare.
Safe by default. Set DRY_RUN=false to let it write.
"""
import os
import logging
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("www_apex_mismatch")
DNS_DOMAIN = os.environ.get("DNS_DOMAIN", "example.com")
CLOUDFLARE_API_TOKEN = os.environ.get("CLOUDFLARE_API_TOKEN", "")
CLOUDFLARE_ZONE_ID = os.environ.get("CLOUDFLARE_ZONE_ID", "")
DRY_RUN = os.environ.get("DRY_RUN", "true").lower() == "true"
CF_API = "https://api.cloudflare.com/client/v4"
def diagnose_www_apex(apex_ips, apex_cname, www_ips, www_cname):
"""Pure decision function. No I/O.
apex_ips/www_ips are resolved IP sets (empty set = NXDOMAIN/no record).
apex_cname/www_cname are the CNAME target strings if present, else None.
Returns one of: "ok", "apex_missing", "www_missing", "both_missing", "ip_mismatch"
"""
apex_ok = bool(apex_ips) or bool(apex_cname)
www_ok = bool(www_ips) or bool(www_cname)
if not apex_ok and not www_ok:
return "both_missing"
if not apex_ok:
return "apex_missing"
if not www_ok:
return "www_missing"
if apex_ips and www_ips and apex_ips.isdisjoint(www_ips):
return "ip_mismatch"
return "ok"
def resolve_name(name):
"""Resolve A, AAAA, and CNAME for a name. Requires network."""
import dns.resolver
resolver = dns.resolver.Resolver()
ips = set()
cname = None
for rtype in ("A", "AAAA"):
try:
answer = resolver.resolve(name, rtype)
ips.update(str(r) for r in answer)
except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN):
pass
except Exception as exc:
log.warning("Query for %s %s failed: %s", name, rtype, exc)
try:
answer = resolver.resolve(name, "CNAME")
cname = str(answer[0].target).rstrip(".")
except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN):
pass
except Exception as exc:
log.warning("Query for %s CNAME failed: %s", name, exc)
return ips, cname
def find_record_id(name, rtype):
"""Find an existing record via the Cloudflare API, or None."""
import requests
headers = {"Authorization": f"Bearer {CLOUDFLARE_API_TOKEN}"}
url = f"{CF_API}/zones/{CLOUDFLARE_ZONE_ID}/dns_records"
r = requests.get(url, headers=headers, params={"type": rtype, "name": name}, timeout=30)
r.raise_for_status()
result = r.json().get("result", [])
return result[0]["id"] if result else None
def create_record(rtype, name, content):
"""Create the missing record through the Cloudflare API."""
import requests
headers = {"Authorization": f"Bearer {CLOUDFLARE_API_TOKEN}", "Content-Type": "application/json"}
url = f"{CF_API}/zones/{CLOUDFLARE_ZONE_ID}/dns_records"
if DRY_RUN:
log.info("[dry run] would create %s record %s -> %s", rtype, name, content)
return
requests.post(
url,
headers=headers,
json={"type": rtype, "name": name, "content": content, "ttl": 300, "proxied": False},
timeout=30,
).raise_for_status()
log.info("Created %s record %s -> %s", rtype, name, content)
def run():
apex = DNS_DOMAIN
www = f"www.{DNS_DOMAIN}"
apex_ips, apex_cname = resolve_name(apex)
www_ips, www_cname = resolve_name(www)
verdict = diagnose_www_apex(apex_ips, apex_cname, www_ips, www_cname)
log.info("Diagnosis for %s / %s: %s", apex, www, verdict)
if verdict == "ok":
log.info("Nothing to repair.")
return
if not (CLOUDFLARE_API_TOKEN and CLOUDFLARE_ZONE_ID):
log.warning("Mismatch found but no Cloudflare credentials set. Skipping repair.")
return
if verdict == "apex_missing" and www_ips:
replacement_ip = os.environ.get("REPLACEMENT_IP", next(iter(www_ips)))
if not find_record_id(apex, "A"):
create_record("A", apex, replacement_ip)
elif verdict == "www_missing" and (apex_ips or apex_cname):
target = apex_cname or apex
if not find_record_id(www, "CNAME"):
create_record("CNAME", www, target)
else:
log.warning("Verdict %s needs a manual decision on which side is correct.", verdict)
if __name__ == "__main__":
run()
/**
* Detect a www/apex DNS mismatch and optionally repair it via Cloudflare.
* Safe by default. Set DRY_RUN=false to let it write.
*/
import { pathToFileURL } from "node:url";
const DNS_DOMAIN = process.env.DNS_DOMAIN || "example.com";
const CLOUDFLARE_API_TOKEN = process.env.CLOUDFLARE_API_TOKEN || "";
const CLOUDFLARE_ZONE_ID = process.env.CLOUDFLARE_ZONE_ID || "";
const DRY_RUN = (process.env.DRY_RUN || "true").toLowerCase() === "true";
const CF_API = "https://api.cloudflare.com/client/v4";
/**
* Pure decision function. No I/O.
*
* apexIps/wwwIps are Sets of resolved IPs (empty set = NXDOMAIN/no record).
* apexCname/wwwCname are the CNAME target strings if present, else null.
*
* Returns one of: "ok", "apex_missing", "www_missing", "both_missing", "ip_mismatch"
*/
export function diagnoseWwwApex(apexIps, apexCname, wwwIps, wwwCname) {
const apexOk = apexIps.size > 0 || Boolean(apexCname);
const wwwOk = wwwIps.size > 0 || Boolean(wwwCname);
if (!apexOk && !wwwOk) return "both_missing";
if (!apexOk) return "apex_missing";
if (!wwwOk) return "www_missing";
if (apexIps.size > 0 && wwwIps.size > 0) {
const disjoint = [...apexIps].every((ip) => !wwwIps.has(ip));
if (disjoint) return "ip_mismatch";
}
return "ok";
}
/** Resolve A, AAAA, and CNAME for a name. Requires network. */
async function resolveName(name) {
const dns = await import("node:dns/promises");
const ips = new Set();
let cname = null;
for (const type of ["A", "AAAA"]) {
try {
const answer = await dns.resolve(name, type);
answer.forEach((ip) => ips.add(ip));
} catch {
// no answer for this type, keep going
}
}
try {
const answer = await dns.resolveCname(name);
if (answer.length) cname = answer[0].replace(/\.$/, "");
} catch {
// no CNAME present
}
return { ips, cname };
}
/** Find an existing record via the Cloudflare API, or null. */
async function findRecordId(name, type) {
const url = `${CF_API}/zones/${CLOUDFLARE_ZONE_ID}/dns_records?type=${type}&name=${name}`;
const res = await fetch(url, { headers: { Authorization: `Bearer ${CLOUDFLARE_API_TOKEN}` } });
if (!res.ok) throw new Error(`Cloudflare list failed: ${res.status}`);
const body = await res.json();
const result = body.result || [];
return result.length ? result[0].id : null;
}
/** Create the missing record through the Cloudflare API. */
async function createRecord(type, name, content) {
if (DRY_RUN) {
console.log(`[dry run] would create ${type} record ${name} -> ${content}`);
return;
}
const res = await fetch(`${CF_API}/zones/${CLOUDFLARE_ZONE_ID}/dns_records`, {
method: "POST",
headers: {
Authorization: `Bearer ${CLOUDFLARE_API_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ type, name, content, ttl: 300, proxied: false }),
});
if (!res.ok) throw new Error(`Cloudflare create failed: ${res.status}`);
console.log(`Created ${type} record ${name} -> ${content}`);
}
async function run() {
const apex = DNS_DOMAIN;
const www = `www.${DNS_DOMAIN}`;
const { ips: apexIps, cname: apexCname } = await resolveName(apex);
const { ips: wwwIps, cname: wwwCname } = await resolveName(www);
const verdict = diagnoseWwwApex(apexIps, apexCname, wwwIps, wwwCname);
console.log(`Diagnosis for ${apex} / ${www}: ${verdict}`);
if (verdict === "ok") {
console.log("Nothing to repair.");
return;
}
if (!(CLOUDFLARE_API_TOKEN && CLOUDFLARE_ZONE_ID)) {
console.warn("Mismatch found but no Cloudflare credentials set. Skipping repair.");
return;
}
if (verdict === "apex_missing" && wwwIps.size > 0) {
const replacementIp = process.env.REPLACEMENT_IP || [...wwwIps][0];
if (!(await findRecordId(apex, "A"))) {
await createRecord("A", apex, replacementIp);
}
} else if (verdict === "www_missing" && (apexIps.size > 0 || apexCname)) {
const target = apexCname || apex;
if (!(await findRecordId(www, "CNAME"))) {
await createRecord("CNAME", www, target);
}
} else {
console.warn(`Verdict ${verdict} needs a manual decision on which side is correct.`);
}
}
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 diagnosis rule is the part worth testing, because it decides whether the script thinks your domain is healthy. It takes plain sets and strings, no network, no DNS lookups, so the tests run in milliseconds.
from www_apex_mismatch import diagnose_www_apex
def test_ok_when_both_resolve_to_same_ips():
ips = {"185.199.108.153", "185.199.109.153"}
assert diagnose_www_apex(ips, None, ips, None) == "ok"
def test_apex_missing_when_apex_has_nothing():
assert diagnose_www_apex(set(), None, {"185.199.108.153"}, None) == "apex_missing"
def test_www_missing_when_www_has_nothing():
assert diagnose_www_apex({"185.199.108.153"}, None, set(), None) == "www_missing"
def test_both_missing_when_neither_resolves():
assert diagnose_www_apex(set(), None, set(), None) == "both_missing"
def test_ip_mismatch_when_disjoint_ip_sets():
apex_ips = {"34.102.136.180"}
www_ips = {"185.199.108.153"}
assert diagnose_www_apex(apex_ips, None, www_ips, None) == "ip_mismatch"
def test_ok_when_apex_uses_cname_alias_only():
assert diagnose_www_apex(set(), "www.example.com", {"185.199.108.153"}, None) == "ok"
import { test } from "node:test";
import assert from "node:assert/strict";
import { diagnoseWwwApex } from "./www-apex-mismatch.js";
test("ok when both resolve to same ips", () => {
const ips = new Set(["185.199.108.153", "185.199.109.153"]);
assert.equal(diagnoseWwwApex(ips, null, ips, null), "ok");
});
test("apex missing when apex has nothing", () => {
const wwwIps = new Set(["185.199.108.153"]);
assert.equal(diagnoseWwwApex(new Set(), null, wwwIps, null), "apex_missing");
});
test("www missing when www has nothing", () => {
const apexIps = new Set(["185.199.108.153"]);
assert.equal(diagnoseWwwApex(apexIps, null, new Set(), null), "www_missing");
});
test("both missing when neither resolves", () => {
assert.equal(diagnoseWwwApex(new Set(), null, new Set(), null), "both_missing");
});
test("ip mismatch when disjoint ip sets", () => {
const apexIps = new Set(["34.102.136.180"]);
const wwwIps = new Set(["185.199.108.153"]);
assert.equal(diagnoseWwwApex(apexIps, null, wwwIps, null), "ip_mismatch");
});
test("ok when apex uses cname alias only", () => {
const wwwIps = new Set(["185.199.108.153"]);
assert.equal(diagnoseWwwApex(new Set(), "www.example.com", wwwIps, null), "ok");
});
Case studies
The launch that only tested www
A small shop launched with a CNAME at www pointing to their storefront platform. Every internal test used the www address, so nobody noticed the apex had no record at all. Months later a print run of business cards listed the bare domain, and every customer typing it in got NXDOMAIN.
Adding A records at the apex, matching the platform's published edge IPs, fixed it the same day the misprint was reported.
The migration that left the apex on the old host
A site moved to a new CDN and updated the www CNAME to the new target, but the apex A records were never touched because they lived in a separate section of the DNS panel. For weeks, www served the new site while the bare domain quietly kept serving the old, retired one.
Comparing dig output for both names side by side showed two completely different IP sets. Pointing the apex A records at the new CDN's IPs, then adding a redirect, brought both names back in line.
A healthy domain answers on both example.com and www.example.com with the same content, no NXDOMAIN on either side, and a clean redirect that sends the non-canonical name to the canonical one. Visitors and search engines never have to guess which address is the real site.
FAQ
Why does my site work on www but not on the bare domain (or the other way around)?
Most sites need two separate DNS records: an A or AAAA record at the bare apex domain, and a CNAME at www pointing to the same host. People often set up one correctly and forget the other, or point them at different backends. The unconfigured name then returns NXDOMAIN, a parked page, or a different, older site.
Can I just put a CNAME at the apex to match www?
No, not a literal one. RFC 1034 says a name with a CNAME cannot carry any other record, and the apex must always carry SOA and NS records. Use A/AAAA records with your host's edge IPs instead, or your provider's CNAME flattening, ALIAS, or ANAME feature if it resolves the target server side.
Should www or the apex be the canonical address?
Either works as long as you pick one and redirect the other to it. Configure both names with working DNS records first, then add a 301 redirect at your host or CDN so visitors and search engines always land on one consistent URL.
Related field notes
Citations
On the problem:
- RFC 1034, Domain Names, Concepts and Facilities, section 3.6.2, the CNAME rule. rfc-editor.org/rfc/rfc1034
- Cloudflare Blog: Zone Apex, Naked Domain, Root Domain CNAME Support. blog.cloudflare.com/zone-apex-naked-domain-root-domain-cname-supp
- AWS: Solving DNS zone apex challenges with third-party DNS providers using AWS. aws.amazon.com/blogs/networking-and-content-delivery
On the solution:
- Cloudflare DNS docs: CNAME flattening. developers.cloudflare.com/dns/cname-flattening
- Cloudflare DNS docs: Create a zone apex record. developers.cloudflare.com/dns/manage-dns-records/how-to/create-zone-apex
- Cloudflare API: DNS Records endpoints. developers.cloudflare.com/api/resources/dns_records
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 domain?
If this saved you a broken launch or a pile of lost visitors, 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