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.

Core DNS records Python and Node.js Safe by default (dry run)
A server room aisle
Photo by Ismail Enes Ayhan on Unsplash
The short answer

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.

www.example.com CNAME configured Real site HTTP/2 200 example.com apex, never configured NXDOMAIN or parked page Different IP no redirect either Lost visitors and broken links Same real IPs, correctly served this name was never the problem
www resolves and serves the real site. The apex was never given a matching record, so it fails or serves the wrong thing while nobody notices for weeks.

Why it happens

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.

The key insight

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.

example.com A / AAAA records www.example.com CNAME record Same host same edge IPs 301 redirect non-canonical -> canonical One site
Both names resolve to the same host, then a redirect makes one of them the single canonical address visitors and search engines land on.

How to fix it

1

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.

Terminal
check-both.sh
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
2

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.

Terminal
check-http.sh
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
3

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.

DNS record
apex-records.txt
# 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
4

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.

DNS record
www-record.txt
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
5

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.

Redirect rule
redirect-rule.txt
# 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
Lower the TTL while you work

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.

Terminal
verify.sh
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.

www_apex_mismatch.py
"""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()
www-apex-mismatch.js
/**
 * 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.

test_www_apex_diagnose.py
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"
www-apex-diagnose.test.js
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

Business cards

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.

CDN migration

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.

What good looks like

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:

  1. RFC 1034, Domain Names, Concepts and Facilities, section 3.6.2, the CNAME rule. rfc-editor.org/rfc/rfc1034
  2. Cloudflare Blog: Zone Apex, Naked Domain, Root Domain CNAME Support. blog.cloudflare.com/zone-apex-naked-domain-root-domain-cname-supp
  3. AWS: Solving DNS zone apex challenges with third-party DNS providers using AWS. aws.amazon.com/blogs/networking-and-content-delivery

On the solution:

  1. Cloudflare DNS docs: CNAME flattening. developers.cloudflare.com/dns/cname-flattening
  2. Cloudflare DNS docs: Create a zone apex record. developers.cloudflare.com/dns/manage-dns-records/how-to/create-zone-apex
  3. 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.

Contact me on LinkedIn

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

Back to all DNS and Domains field notes