Reconciler Core Records
Hosting provider DNS check fails on a custom domain setup
You point your domain at GitHub Pages, Netlify, or Vercel, save the custom domain in the settings, and click check. It comes back "DNS check unsuccessful." No certificate, no traffic, just a red warning. Here is why the check fails, what records it actually wants to see, and how to set them so it turns green.
GitHub Pages and similar hosts verify a custom domain by checking the exact apex A/AAAA records and the exact www CNAME target the host publishes. The check almost always fails because old records from a previous host are still in the zone, only some of the four required apex IPs are present, or www points at the wrong target. A CNAME cannot share a name with any other record, so a stray A or TXT left on www will also break it. The fix is to replace the apex A records with the host's exact four IPs and point www at a single clean CNAME. Full records, commands, and a checker script are below.
The problem in plain words
When you add a custom domain to GitHub Pages, Netlify, or Vercel, the host does not just take your word for it. It looks up your domain's DNS records from the outside, the same way any visitor's browser would, and compares what it finds against the exact values it expects. If the apex domain (the bare example.com, with no subdomain) does not resolve to the host's published IP addresses, or the www subdomain does not point at the host's given hostname, the check fails and stays red.
This almost always comes down to leftover DNS. A domain that used to live on Squarespace, Wix, or a different host often still has that old provider's A record sitting at the apex, or an old CNAME sitting on www, and nobody removed it when the site moved. The new host's check has nothing to disagree with in principle, it is just looking at records that were never updated.
Why it happens
- The zone still has an A record, CNAME, or nameserver pointing at a previous host, such as Squarespace or Wix, that never got removed when the site moved.
- Only one or two of the four required GitHub Pages apex IPs are set, instead of all four, so the host sees a partial and unrecognized match.
- The
wwwCNAME points at the bare username instead of the fullusername.github.iohostname, or at a completely different target. - A stray A, TXT, or MX record is left on
wwwalongside the CNAME. A CNAME must be the only record on that name, so the extra record makes the whole answer invalid. - The edit was made at the DNS provider but the change has not reached public resolvers yet, which looks identical to a wrong record until it propagates.
The host is not guessing. It publishes an exact, public list of the IPs and hostnames it expects, and it checks against that list every time you click check again. Fixing this is not about "adding some DNS," it is about making the zone match that exact published list, and nothing else, on the apex and on www.
The fix, as a flow
We check what the apex and www currently resolve to, compare that against the host's published required set, then replace the wrong or missing records in the zone with the exact expected values. We remove anything left over from an old host on those same names, since old records left in place are what breaks the check in most cases.
How to fix it
Check what the apex currently resolves to
Look up the A records for the bare domain. GitHub Pages requires exactly these four IPs: 185.199.108.153, 185.199.109.153, 185.199.110.153, and 185.199.111.153. A bad result looks like an old host's IP, only one or two of the four, or extra unrelated IPs mixed in with them.
dig +short A example.com
# what a good answer looks like, in any order:
# 185.199.108.153
# 185.199.109.153
# 185.199.110.153
# 185.199.111.153
Check what www currently points at
Look up the CNAME for www. It should return your GitHub Pages hostname with a trailing dot, such as yourusername.github.io., or yourorg.github.io. for an organization site. An empty result means an A record is sitting there instead of a CNAME, and a different target means www points somewhere else entirely.
dig +short CNAME www.example.com
# a good answer looks like:
# yourusername.github.io.
Look for a conflicting record hiding next to the CNAME
A CNAME record cannot coexist with any other record on the same name. If www has a leftover A, TXT, or MX record from an old host sitting alongside the CNAME, that setup is invalid per RFC 1034 section 3.6.2, and the host's check will fail or behave inconsistently even though the CNAME itself looks correct.
dig www.example.com ANY
dig +short A www.example.com
dig +short TXT www.example.com
# from an authoritative angle, to rule out a slow-propagating edit:
dig @ns1.example.com example.com A
Replace the apex records with exactly the host's four IPs
At the DNS provider, delete any existing A records on the apex (@) and add all four of the GitHub Pages IPs. Add the matching AAAA records too if you want IPv6 reachability. Do not add just one or two, the host looks for the full set.
example.com. 300 IN A 185.199.108.153
example.com. 300 IN A 185.199.109.153
example.com. 300 IN A 185.199.110.153
example.com. 300 IN A 185.199.111.153
; optional, for IPv6:
example.com. 300 IN AAAA 2606:50c0:8000::153
example.com. 300 IN AAAA 2606:50c0:8001::153
example.com. 300 IN AAAA 2606:50c0:8002::153
example.com. 300 IN AAAA 2606:50c0:8003::153
Delete any conflicting record on www, then add a single clean CNAME
Remove any A, TXT, or MX record sitting on www, and remove any old CNAME left over from a previous host such as Squarespace or Wix. Then add one CNAME pointing at your full GitHub Pages hostname, with the trailing dot.
www.example.com. 300 IN CNAME yourusername.github.io.
; for an organization site, use the org account instead:
; www.example.com. 300 IN CNAME yourorg.github.io.
Set the custom domain in the repo and click check again
In the GitHub repo, go to Settings, Pages, and set Custom domain to the domain you are using. This commits a CNAME file to the repository. Once the records above are live, click Check again to re-trigger GitHub's verification.
echo "example.com" > CNAME
git add CNAME
git commit -m "Set custom domain for GitHub Pages"
git push
How to check it worked
Recheck both names, this time against public resolvers, not just your provider's own nameservers. Then confirm the host agrees and, once time has passed, confirm HTTPS actually works.
dig +short A example.com
dig +short CNAME www.example.com
dig @8.8.8.8 example.com A
dig @1.1.1.1 example.com A
# after up to 24 hours, confirm the certificate:
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
| openssl x509 -noout -subject -dates
# a good answer looks like exactly these four, no extras:
# 185.199.108.153
# 185.199.109.153
# 185.199.110.153
# 185.199.111.153
In the GitHub repo settings under Pages, the custom domain status should change from "DNS check unsuccessful" to a green "DNS check successful."
The full code
Here is a small script that resolves the apex A records and the www CNAME target, compares them against the host's published required set, and reports exactly which apex IPs are missing or extra and whether www points at the right place. When repair is turned on, it replaces the stale records in Cloudflare with the correct ones. 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 GitHub Pages custom domain DNS check failure 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("custom_domain_dns_check")
GITHUB_PAGES_A_RECORDS = {
"185.199.108.153",
"185.199.109.153",
"185.199.110.153",
"185.199.111.153",
}
def diagnose_pages_dns(apex_a_records: set, www_cname_target, required_a_records: set, expected_cname_suffix: str) -> dict:
"""Pure decision function. No network, no I/O."""
missing = required_a_records - apex_a_records
extra = apex_a_records - required_a_records
apex_ok = not missing and not extra
www_ok = bool(www_cname_target) and www_cname_target.rstrip(".").endswith(expected_cname_suffix.lstrip("."))
return {
"apex_ok": apex_ok,
"apex_missing": missing,
"apex_extra": extra,
"www_ok": www_ok,
"www_target": www_cname_target,
}
def run():
# Imported lazily so the pure function above can be tested with no
# network libraries installed at all.
import dns.resolver
import requests
domain = os.environ["DNS_DOMAIN"]
github_hostname = os.environ.get("GITHUB_PAGES_HOSTNAME", "yourusername.github.io")
dry_run = os.environ.get("DRY_RUN", "true").lower() == "true"
zone_id = os.environ["CLOUDFLARE_ZONE_ID"]
api_token = os.environ["CLOUDFLARE_API_TOKEN"]
try:
apex_answer = dns.resolver.resolve(domain, "A")
apex_a_records = {str(r) for r in apex_answer}
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
apex_a_records = set()
www_name = f"www.{domain}"
try:
cname_answer = dns.resolver.resolve(www_name, "CNAME")
www_cname_target = str(cname_answer[0].target).rstrip(".")
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
www_cname_target = None
report = diagnose_pages_dns(apex_a_records, www_cname_target, GITHUB_PAGES_A_RECORDS, ".github.io")
log.info("Apex ok=%s missing=%s extra=%s", report["apex_ok"], report["apex_missing"], report["apex_extra"])
log.info("www ok=%s target=%s", report["www_ok"], report["www_target"])
if report["apex_ok"] and report["www_ok"]:
log.info("Nothing to repair. DNS matches GitHub Pages requirements.")
return
headers = {"Authorization": f"Bearer {api_token}", "Content-Type": "application/json"}
base = f"https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records"
if not report["apex_ok"]:
log.info("Apex %s is wrong. %s replace the A records with the four GitHub Pages IPs.",
domain, "Would" if dry_run else "Will")
if not dry_run:
existing = requests.get(base, headers=headers, params={"type": "A", "name": domain}, timeout=30).json()
for record in existing.get("result", []):
requests.delete(f"{base}/{record['id']}", headers=headers, timeout=30).raise_for_status()
for ip in sorted(GITHUB_PAGES_A_RECORDS):
requests.post(base, headers=headers, json={"type": "A", "name": domain, "content": ip, "ttl": 300}, timeout=30).raise_for_status()
if not report["www_ok"]:
log.info("www.%s is wrong. %s replace it with a CNAME to %s.",
domain, "Would" if dry_run else "Will", github_hostname)
if not dry_run:
existing = requests.get(base, headers=headers, params={"name": www_name}, timeout=30).json()
for record in existing.get("result", []):
requests.delete(f"{base}/{record['id']}", headers=headers, timeout=30).raise_for_status()
requests.post(base, headers=headers, json={"type": "CNAME", "name": www_name, "content": github_hostname, "ttl": 300}, timeout=30).raise_for_status()
log.info("Done.")
if __name__ == "__main__":
run()
/**
* Detect a GitHub Pages custom domain DNS check failure 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 const GITHUB_PAGES_A_RECORDS = new Set([
"185.199.108.153",
"185.199.109.153",
"185.199.110.153",
"185.199.111.153",
]);
export function diagnosePagesDns(apexARecords, wwwCnameTarget, requiredARecords, expectedCnameSuffix) {
// Pure decision function. No network, no I/O.
const missing = new Set([...requiredARecords].filter((ip) => !apexARecords.has(ip)));
const extra = new Set([...apexARecords].filter((ip) => !requiredARecords.has(ip)));
const apexOk = missing.size === 0 && extra.size === 0;
const suffix = expectedCnameSuffix.replace(/^\./, "");
const wwwOk = Boolean(wwwCnameTarget) && wwwCnameTarget.replace(/\.$/, "").endsWith(suffix);
return {
apex_ok: apexOk,
apex_missing: missing,
apex_extra: extra,
www_ok: wwwOk,
www_target: wwwCnameTarget,
};
}
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 githubHostname = process.env.GITHUB_PAGES_HOSTNAME || "yourusername.github.io";
const dryRun = (process.env.DRY_RUN || "true").toLowerCase() === "true";
const zoneId = process.env.CLOUDFLARE_ZONE_ID;
const apiToken = process.env.CLOUDFLARE_API_TOKEN;
let apexARecords = new Set();
try {
apexARecords = new Set(await resolvePromises.resolve4(domain));
} catch (err) {
if (err.code !== "ENOTFOUND" && err.code !== "ENODATA") throw err;
}
const wwwName = `www.${domain}`;
let wwwCnameTarget = null;
try {
const cnames = await resolvePromises.resolveCname(wwwName);
wwwCnameTarget = cnames[0] || null;
} catch (err) {
if (err.code !== "ENOTFOUND" && err.code !== "ENODATA") throw err;
}
const report = diagnosePagesDns(apexARecords, wwwCnameTarget, GITHUB_PAGES_A_RECORDS, ".github.io");
console.log(`Apex ok=${report.apex_ok} missing=${[...report.apex_missing]} extra=${[...report.apex_extra]}`);
console.log(`www ok=${report.www_ok} target=${report.www_target}`);
if (report.apex_ok && report.www_ok) {
console.log("Nothing to repair. DNS matches GitHub Pages requirements.");
return;
}
const headers = { Authorization: `Bearer ${apiToken}`, "Content-Type": "application/json" };
const base = `https://api.cloudflare.com/client/v4/zones/${zoneId}/dns_records`;
if (!report.apex_ok) {
console.log(`Apex ${domain} is wrong. ${dryRun ? "Would" : "Will"} replace the A records with the four GitHub Pages IPs.`);
if (!dryRun) {
const existing = await (await fetch(`${base}?type=A&name=${domain}`, { headers })).json();
for (const record of existing.result || []) {
await fetch(`${base}/${record.id}`, { method: "DELETE", headers });
}
for (const ip of [...GITHUB_PAGES_A_RECORDS].sort()) {
await fetch(base, { method: "POST", headers, body: JSON.stringify({ type: "A", name: domain, content: ip, ttl: 300 }) });
}
}
}
if (!report.www_ok) {
console.log(`www.${domain} is wrong. ${dryRun ? "Would" : "Will"} replace it with a CNAME to ${githubHostname}.`);
if (!dryRun) {
const existing = await (await fetch(`${base}?name=${wwwName}`, { headers })).json();
for (const record of existing.result || []) {
await fetch(`${base}/${record.id}`, { method: "DELETE", headers });
}
await fetch(base, { method: "POST", headers, body: JSON.stringify({ type: "CNAME", name: wwwName, content: githubHostname, ttl: 300 }) });
}
}
console.log("Done.");
}
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
run().catch((err) => { console.error(err); process.exit(1); });
}
Add a test
The comparison logic is the part worth testing on its own, because it decides which records the script thinks are wrong. It takes plain sets and strings, so the test needs no network and no DNS library at all.
from custom_domain_dns_check import diagnose_pages_dns, GITHUB_PAGES_A_RECORDS
def test_all_ok_when_records_match():
report = diagnose_pages_dns(GITHUB_PAGES_A_RECORDS, "yourusername.github.io", GITHUB_PAGES_A_RECORDS, ".github.io")
assert report["apex_ok"] is True
assert report["www_ok"] is True
def test_apex_missing_ips_reported():
partial = {"185.199.108.153", "185.199.109.153"}
report = diagnose_pages_dns(partial, "yourusername.github.io", GITHUB_PAGES_A_RECORDS, ".github.io")
assert report["apex_ok"] is False
assert report["apex_missing"] == {"185.199.110.153", "185.199.111.153"}
def test_apex_extra_ip_reported():
extra_set = GITHUB_PAGES_A_RECORDS | {"203.0.113.10"}
report = diagnose_pages_dns(extra_set, "yourusername.github.io", GITHUB_PAGES_A_RECORDS, ".github.io")
assert report["apex_ok"] is False
assert report["apex_extra"] == {"203.0.113.10"}
def test_www_wrong_target_reported():
report = diagnose_pages_dns(GITHUB_PAGES_A_RECORDS, "old-host.example.net", GITHUB_PAGES_A_RECORDS, ".github.io")
assert report["www_ok"] is False
def test_www_missing_reported():
report = diagnose_pages_dns(GITHUB_PAGES_A_RECORDS, None, GITHUB_PAGES_A_RECORDS, ".github.io")
assert report["www_ok"] is False
import { test } from "node:test";
import assert from "node:assert/strict";
import { diagnosePagesDns, GITHUB_PAGES_A_RECORDS } from "./custom-domain-dns-check.js";
test("all ok when records match", () => {
const report = diagnosePagesDns(GITHUB_PAGES_A_RECORDS, "yourusername.github.io", GITHUB_PAGES_A_RECORDS, ".github.io");
assert.equal(report.apex_ok, true);
assert.equal(report.www_ok, true);
});
test("apex missing ips reported", () => {
const partial = new Set(["185.199.108.153", "185.199.109.153"]);
const report = diagnosePagesDns(partial, "yourusername.github.io", GITHUB_PAGES_A_RECORDS, ".github.io");
assert.equal(report.apex_ok, false);
assert.deepEqual([...report.apex_missing].sort(), ["185.199.110.153", "185.199.111.153"]);
});
test("apex extra ip reported", () => {
const extraSet = new Set([...GITHUB_PAGES_A_RECORDS, "203.0.113.10"]);
const report = diagnosePagesDns(extraSet, "yourusername.github.io", GITHUB_PAGES_A_RECORDS, ".github.io");
assert.equal(report.apex_ok, false);
assert.deepEqual([...report.apex_extra], ["203.0.113.10"]);
});
test("www wrong target reported", () => {
const report = diagnosePagesDns(GITHUB_PAGES_A_RECORDS, "old-host.example.net", GITHUB_PAGES_A_RECORDS, ".github.io");
assert.equal(report.www_ok, false);
});
test("www missing reported", () => {
const report = diagnosePagesDns(GITHUB_PAGES_A_RECORDS, null, GITHUB_PAGES_A_RECORDS, ".github.io");
assert.equal(report.www_ok, false);
});
Case studies
The domain that kept two hosts arguing over www
A small business moved its site from Squarespace to a GitHub Pages site but only updated the apex A records during the move. The www subdomain still carried Squarespace's old A record. GitHub Pages saw a CNAME was needed but found an A record instead, and the check stayed red for a week.
Deleting the leftover A record on www and adding the single CNAME to username.github.io turned the check green within minutes of the change reaching public resolvers.
The apex that only had two of the four IPs
A developer copied GitHub's setup guide into their DNS provider but pasted only the first two A records, then got interrupted. The site loaded fine for most visitors because some requests happened to land on a working IP, but the DNS check kept failing since it wanted the complete set of four.
Running a diagnostic script during the incident showed exactly which two IPs were missing, so the fix was one clean batch of additions instead of more guesswork.
Once the records match, dig +short A example.com returns exactly the four GitHub Pages IPs and nothing else, dig +short CNAME www.example.com returns your GitHub Pages hostname, and the repo's Pages settings show a green "DNS check successful." Within about 24 hours, a valid Let's Encrypt certificate is issued and HTTPS enforcement becomes available.
FAQ
Why does GitHub Pages say the DNS check is unsuccessful for my domain?
GitHub Pages checks that your apex domain has exactly the four GitHub A records and that www has a CNAME pointing at your username.github.io. The check fails when old records from a previous host are still there, only some of the four IPs are present, or www points at the wrong target or has a conflicting record on the same name.
Can a CNAME record and an A record exist on the same name at once?
No. A CNAME must be the only record on that exact name. If www has a CNAME plus a leftover A, TXT, or MX record from an old host, that is invalid per RFC 1034 and will make the DNS check fail or behave inconsistently.
How long does it take for the DNS check to pass after I fix the records?
Once the correct records are live and visible to public resolvers, clicking Check again in the host's settings usually confirms it within minutes. Full propagation to every resolver can take up to the old record's TTL, and the HTTPS certificate can take up to 24 hours to issue after the check passes.
Related field notes
Citations
On the problem:
- GitHub Docs: Troubleshooting custom domains and GitHub Pages. docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site/troubleshooting-custom-domains-and-github-pages
- GitHub Docs: About custom domains and GitHub Pages. docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site/about-custom-domains-and-github-pages
- RFC 1034: Domain Names, Concepts and Facilities, section 3.6.2 on CNAME restrictions. rfc-editor.org/rfc/rfc1034
On the solution:
- GitHub Docs: Managing a custom domain for your GitHub Pages site. docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site/managing-a-custom-domain-for-your-github-pages-site
- Cloudflare API: DNS records for a zone, list. developers.cloudflare.com/api/operations/dns-records-for-a-zone-list-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 DNS check?
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