Reconciler Nameserver / Delegation

Hosted zone recreated with new NS values

You deleted a hosted zone and made a new one with the same name, thinking it would pick up right where the old one left off. Instead the domain stopped resolving. The new zone got a brand new, random set of nameservers, and your registrar is still holding onto the old ones. Here is why that happens, and how to point the registrar at the right servers again.

Python and Node.js RDAP + dnspython Diagnostic only
Green and white electric device
Photo by Kirill Sh on Unsplash
The short answer

Deleting a hosted zone and creating a new one with the same domain name does not give you the same nameservers back. Route 53, Cloudflare, and every other provider hand the new zone a fresh, randomly picked set of nameservers. Your registrar still lists the old ones from the zone you deleted, and nothing links the two automatically. The domain keeps resolving through stale nameservers, which now serve nothing or belong to a different zone entirely, until a person updates the registrar's nameserver list to match the new zone. A script can detect this mismatch by comparing the zone's live nameservers against what the registrar has delegated, but only a person can save the fix at the registrar.

The problem in plain words

A hosted zone is not a fixed address. Every time a DNS provider creates one, whether it is brand new or a replacement for one you just deleted, it hands out a nameserver set almost at random from a large pool. The old zone's nameservers belonged to the old zone, and once that zone is gone, those names either answer for nobody or, worse, get reassigned to some other customer's zone later on.

Your registrar has no idea any of this happened. It keeps publishing the nameserver names you told it to publish the last time you touched the settings, which are the old zone's names. So when someone looks up your domain, the registry still points them at the old nameservers. Those servers either have no idea what your domain is anymore, or they answer for someone else's zone, and either way your real records, sitting safely in the new zone, are never reached.

Resolver looks up example.com Registrar record unchanged since deletion points at deleted zone Stale nameservers serve nobody, or someone else Recreated zone correct records, new NS set NXDOMAIN or SERVFAIL
The registrar never learned the zone was recreated, so it keeps sending resolvers to nameservers that no longer have anything to do with your domain.

Why it happens

Delegation is controlled at the parent, by the registrar's NS glue at the TLD servers, not by whatever the DNS provider is doing internally. A few things line up to cause this:

The result is a silent split. Your zone is fine. Your registrar is fine. They are just no longer talking about the same nameservers, and nothing forces them to notice.

The key insight

A hosted zone's nameservers are not a property of the domain name, they are a property of that specific zone object. Delete the zone and you lose that nameserver set for good. Recreating the zone creates a new one from scratch with a new random set, and the registrar has to be told about it by hand, every single time.

The fix, as a flow

Get the exact nameservers the new zone was actually assigned, straight from the provider, then replace the old list at the registrar with those exact values. This is purely a registrar-side change. Never edit the zone's own NS record set to try to match the registrar, the provider manages that automatically and will overwrite or ignore the edit.

Read new zone's nameservers from the API Open registrar's nameserver settings Replace old list paste the exact new values Save the registrar change Wait for registry
Nothing inside the zone changes. Only the registrar's nameserver list moves, to match whatever the new zone was actually assigned.

How to fix it

1

Get the new zone's exact nameservers from the provider

Do not guess or reuse the old values. Ask the provider directly for the nameserver set the new zone was actually assigned. For Route 53, use the AWS CLI or console. For Cloudflare, use the dashboard's Overview page or the API.

Terminal
get-new-nameservers.sh
# Route 53: read the new hosted zone's assigned nameservers
aws route53 get-hosted-zone --id Z0123456789ABC --query "DelegationSet.NameServers"

# Example output
# [
#   "ns-321.awsdns-40.com",
#   "ns-1054.awsdns-04.org",
#   "ns-1587.awsdns-05.co.uk",
#   "ns-789.awsdns-08.net"
# ]

# Cloudflare: read the zone's nameserver pair
curl -s -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
  "https://api.cloudflare.com/client/v4/zones/$CLOUDFLARE_ZONE_ID" \
  | jq '.result.name_servers'
2

Log into the registrar and open the nameserver settings

This is a registrar-side change, not a zone record change. Go to GoDaddy, Namecheap, the Route 53 registrar console, or wherever the domain is registered, and find the domain's nameserver settings.

3

Delete the old entries and paste in the new ones exactly

Remove every old nameserver entry first, then paste in the values from step 1, one per field. Copy and paste, do not retype by hand, since a single typo produces a delegation that resolves to nowhere.

DNS record
example.com registrar nameserver list (before and after)
; Before: registrar still lists the deleted zone's old nameservers
example.com.  NS  ns-1.awsdns-00.com.
example.com.  NS  ns-2.awsdns-00.net.
example.com.  NS  ns-3.awsdns-00.org.
example.com.  NS  ns-4.awsdns-00.co.uk.

; After: registrar updated to the recreated zone's actual nameservers
example.com.  NS  ns-321.awsdns-40.com.
example.com.  NS  ns-1054.awsdns-04.org.
example.com.  NS  ns-1587.awsdns-05.co.uk.
example.com.  NS  ns-789.awsdns-08.net.
4

Do not touch the zone's own NS record set

It can be tempting to edit the new zone's NS records to match the old registrar values instead. Do not. The provider manages the zone's own NS record set automatically, and any manual edit there is overwritten or simply ignored. The fix only ever flows from the zone's real nameservers into the registrar, never the other way.

5

Save and let the registry publish the change

Once saved, the registry needs to pick up the new delegation. This is usually fast, but plan for up to 24 to 48 hours for every resolver everywhere to have a fresh answer, since it depends on cached TTLs around the world.

How to check it worked

Compare what the registrar publishes against what the new zone actually answers with. They should be the exact same set of names.

Terminal
verify.sh
# 1. What the new zone itself is serving (query one of its own nameservers)
dig +short NS example.com @ns-321.awsdns-40.com

# 2. What the registrar has delegated (via the recursive resolver, or RDAP)
dig +short NS example.com
whois example.com | grep -i "name server"
curl -s https://rdap.org/domain/example.com | jq '.nameservers[].ldhName'

# 3. Confirm the two sets are the same names, case-insensitive, order does not matter

# 4. Trace the full delegation path from the root down
dig +trace example.com

# 5. Confirm a real record resolves through the new nameservers
dig +short A example.com @ns-321.awsdns-40.com
dig +short A example.com @8.8.8.8

A good result looks like this: step 1 and step 2 return the identical set of nameserver hostnames. The trace in step 4 shows the parent TLD servers referring straight to the new zone's nameservers, with no NXDOMAIN or SERVFAIL anywhere along the path. Step 5 returns the expected IP address both from the new zone directly and, once caches catch up, from a public resolver like 8.8.8.8 too.

The full code

This problem is a diagnostic, not a repair. Fixing the delegation is a registrar-portal action, outside anything the Cloudflare or Route 53 DNS-records API can reach, so the script below only detects and reports the mismatch. It fetches the zone's live nameservers from the provider API and compares them, as a case-insensitive set, against the registrar-delegated nameservers from RDAP or a plain NS query.

View this code on GitHub Full runnable folder with tests in the dns-fixes repo.

check_recreated_zone_ns.py
"""Detect a mismatch between a recreated hosted zone's live nameservers and
the nameservers the registrar still has delegated. Diagnostic only: fixing
the registrar's delegation is a registrar-portal action, not something the
Cloudflare DNS API can do, so this script never writes anything.
"""
import os
import logging

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("check_recreated_zone_ns")

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"


def normalize_ns(hostname):
    return hostname.strip().lower().rstrip(".")


def ns_sets_match(zone_ns, registrar_ns):
    """Pure function, no I/O. Normalize each hostname (strip the trailing
    dot, lowercase), convert both lists to sets, and compare them. True
    means the delegation matches. False means the registrar is stale."""
    return {normalize_ns(h) for h in zone_ns} == {normalize_ns(h) for h in registrar_ns}


def get_zone_ns(zone_id):
    """The provider's own view: the live nameservers the recreated zone was
    actually assigned. Uses the Cloudflare API here; Route 53 users can swap
    in aws route53 get-hosted-zone instead."""
    import requests

    url = f"https://api.cloudflare.com/client/v4/zones/{zone_id}"
    r = requests.get(url, headers={"Authorization": f"Bearer {CLOUDFLARE_API_TOKEN}"}, timeout=15)
    r.raise_for_status()
    return r.json()["result"]["name_servers"]


def get_registrar_ns(domain):
    """RDAP is the registry-facing view: what the registrar has delegated."""
    import requests

    r = requests.get(f"https://rdap.org/domain/{domain}", timeout=15)
    r.raise_for_status()
    data = r.json()
    return [ns["ldhName"] for ns in data.get("nameservers", []) if ns.get("ldhName")]


def run():
    log.info("Checking nameserver delegation for %s (DRY_RUN=%s)", DNS_DOMAIN, DRY_RUN)

    zone_ns = get_zone_ns(CLOUDFLARE_ZONE_ID)
    registrar_ns = get_registrar_ns(DNS_DOMAIN)

    log.info("Zone (provider API) nameservers: %s", sorted(zone_ns))
    log.info("Registrar (RDAP) nameservers: %s", sorted(registrar_ns))

    if ns_sets_match(zone_ns, registrar_ns):
        log.info("OK: registrar delegation matches the recreated zone. Nothing to do.")
        return

    log.warning(
        "MISMATCH: the zone now answers with %s but the registrar still delegates to %s. "
        "This looks like a hosted zone that was deleted and recreated without updating the "
        "registrar. Update the nameserver list at the registrar to the zone's current values.",
        sorted(zone_ns), sorted(registrar_ns),
    )

    # Note for future readers: fixing this is a registrar-portal action. The
    # Cloudflare DNS API at https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records
    # only manages records inside a zone already delegated to it, it has no
    # endpoint that can touch what the registrar publishes to the registry.
    if not DRY_RUN:
        log.info("DRY_RUN is false, but this check never writes. Fix the registrar by hand.")


if __name__ == "__main__":
    run()
check-recreated-zone-ns.js
/**
 * Detect a mismatch between a recreated hosted zone's live nameservers and
 * the nameservers the registrar still has delegated. Diagnostic only:
 * fixing the registrar's delegation is a registrar-portal action, not
 * something the Cloudflare DNS API can do, so this script never writes
 * anything.
 */
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";

export function normalizeNs(hostname) {
  return hostname.trim().toLowerCase().replace(/\.$/, "");
}

export function nsSetsMatch(zoneNs, registrarNs) {
  // Pure function, no I/O. Normalize each hostname and compare as sets.
  const a = new Set(zoneNs.map(normalizeNs));
  const b = new Set(registrarNs.map(normalizeNs));
  if (a.size !== b.size) return false;
  for (const host of a) if (!b.has(host)) return false;
  return true;
}

async function getZoneNs(zoneId) {
  // The provider's own view: the live nameservers the recreated zone was
  // actually assigned. Uses the Cloudflare API here; Route 53 users can
  // swap in aws route53 get-hosted-zone instead.
  const res = await fetch(`https://api.cloudflare.com/client/v4/zones/${zoneId}`, {
    headers: { Authorization: `Bearer ${CLOUDFLARE_API_TOKEN}` },
  });
  if (!res.ok) throw new Error(`Cloudflare zone lookup failed: ${res.status}`);
  const data = await res.json();
  return data.result.name_servers;
}

async function getRegistrarNs(domain) {
  // RDAP is the registry-facing view: what the registrar has delegated.
  const res = await fetch(`https://rdap.org/domain/${domain}`);
  if (!res.ok) throw new Error(`RDAP lookup failed: ${res.status}`);
  const data = await res.json();
  return (data.nameservers || []).map((ns) => ns.ldhName).filter(Boolean);
}

export async function run() {
  console.log(`Checking nameserver delegation for ${DNS_DOMAIN} (DRY_RUN=${DRY_RUN})`);

  const zoneNs = await getZoneNs(CLOUDFLARE_ZONE_ID);
  const registrarNs = await getRegistrarNs(DNS_DOMAIN);

  console.log("Zone (provider API) nameservers:", [...zoneNs].sort());
  console.log("Registrar (RDAP) nameservers:", [...registrarNs].sort());

  if (nsSetsMatch(zoneNs, registrarNs)) {
    console.log("OK: registrar delegation matches the recreated zone. Nothing to do.");
    return;
  }

  console.warn(
    `MISMATCH: the zone now answers with ${JSON.stringify([...zoneNs].sort())} ` +
    `but the registrar still delegates to ${JSON.stringify([...registrarNs].sort())}. ` +
    "This looks like a hosted zone that was deleted and recreated without updating the " +
    "registrar. Update the nameserver list at the registrar to the zone's current values."
  );

  // Note for future readers: fixing this is a registrar-portal action. The
  // Cloudflare DNS API at https://api.cloudflare.com/client/v4/zones/{zone_id}/dns_records
  // only manages records inside a zone already delegated to it, it has no
  // endpoint that can touch what the registrar publishes to the registry.
  if (!DRY_RUN) {
    console.log("DRY_RUN is false, but this check never writes. Fix the registrar by hand.");
  }
}

if (import.meta.url === pathToFileURL(process.argv[1]).href) {
  run().catch((e) => { console.error(e); process.exit(1); });
}

Add a test

The comparison itself is what is worth testing, since it decides whether the script reports a mismatch. It takes plain lists of hostnames in and returns true or false, so the test needs no network and no real domain.

test_hosted_ns_match.py
from check_recreated_zone_ns import ns_sets_match


def test_matches_when_identical():
    assert ns_sets_match(["ns1.example.com"], ["ns1.example.com"]) is True


def test_matches_ignoring_case():
    assert ns_sets_match(["NS-321.AWSDNS-40.COM"], ["ns-321.awsdns-40.com"]) is True


def test_matches_ignoring_trailing_dot():
    assert ns_sets_match(["ns-321.awsdns-40.com."], ["ns-321.awsdns-40.com"]) is True


def test_matches_ignoring_order():
    a = ["ns-321.awsdns-40.com", "ns-1054.awsdns-04.org"]
    b = ["ns-1054.awsdns-04.org", "ns-321.awsdns-40.com"]
    assert ns_sets_match(a, b) is True


def test_mismatch_on_recreated_zone_vs_stale_registrar():
    new_zone = ["ns-321.awsdns-40.com", "ns-1054.awsdns-04.org"]
    old_registrar = ["ns-1.awsdns-00.com", "ns-2.awsdns-00.net"]
    assert ns_sets_match(new_zone, old_registrar) is False


def test_mismatch_when_one_list_has_an_extra_server():
    a = ["ns1.example.com", "ns2.example.com"]
    b = ["ns1.example.com", "ns2.example.com", "ns3.example.com"]
    assert ns_sets_match(a, b) is False


def test_mismatch_when_one_list_is_missing_a_server():
    a = ["ns1.example.com", "ns2.example.com"]
    b = ["ns1.example.com"]
    assert ns_sets_match(a, b) is False


def test_both_empty_counts_as_matching():
    assert ns_sets_match([], []) is True


def test_one_empty_one_not_is_a_mismatch():
    assert ns_sets_match([], ["ns1.example.com"]) is False
hosted-ns-match.test.js
import { test } from "node:test";
import assert from "node:assert/strict";
import { nsSetsMatch } from "./check-recreated-zone-ns.js";

test("matches when identical", () => {
  assert.equal(nsSetsMatch(["ns1.example.com"], ["ns1.example.com"]), true);
});

test("matches ignoring case", () => {
  assert.equal(nsSetsMatch(["NS-321.AWSDNS-40.COM"], ["ns-321.awsdns-40.com"]), true);
});

test("matches ignoring trailing dot", () => {
  assert.equal(nsSetsMatch(["ns-321.awsdns-40.com."], ["ns-321.awsdns-40.com"]), true);
});

test("matches ignoring order", () => {
  const a = ["ns-321.awsdns-40.com", "ns-1054.awsdns-04.org"];
  const b = ["ns-1054.awsdns-04.org", "ns-321.awsdns-40.com"];
  assert.equal(nsSetsMatch(a, b), true);
});

test("mismatch on recreated zone vs stale registrar", () => {
  const newZone = ["ns-321.awsdns-40.com", "ns-1054.awsdns-04.org"];
  const oldRegistrar = ["ns-1.awsdns-00.com", "ns-2.awsdns-00.net"];
  assert.equal(nsSetsMatch(newZone, oldRegistrar), false);
});

test("mismatch when one list has an extra server", () => {
  const a = ["ns1.example.com", "ns2.example.com"];
  const b = ["ns1.example.com", "ns2.example.com", "ns3.example.com"];
  assert.equal(nsSetsMatch(a, b), false);
});

test("mismatch when one list is missing a server", () => {
  const a = ["ns1.example.com", "ns2.example.com"];
  const b = ["ns1.example.com"];
  assert.equal(nsSetsMatch(a, b), false);
});

test("both empty counts as matching", () => {
  assert.equal(nsSetsMatch([], []), true);
});

test("one empty one not is a mismatch", () => {
  assert.equal(nsSetsMatch([], ["ns1.example.com"]), false);
});

Case studies

Route 53

The "clean slate" zone that broke a working domain

An engineer wanted to start fresh, so they deleted a messy Route 53 hosted zone and created a new one with the same domain name, planning to re-add every record by hand. The new zone came back with four nameservers that looked nothing like the old ones. Nobody noticed until customers reported the site was down.

Running aws route53 get-hosted-zone against the new zone showed the real nameservers immediately. Pasting those four values into the registrar's nameserver fields, replacing the stale set, fixed resolution within the hour.

Cloudflare

Reassigned nameservers pointed at a stranger's zone

A team deleted a Cloudflare zone during a migration test and recreated it a day later. The two nameservers Cloudflare handed out the second time were entirely different from the first pair, since Cloudflare draws from a shared pool and does not guarantee the same pair twice.

Worse, one of the old nameserver names had, in the meantime, started answering for a different Cloudflare customer's zone. Anyone still resolving through the stale registrar entry was silently being sent toward somebody else's DNS answers. Updating the registrar to the new pair from the dashboard's Overview page closed the gap.

What good looks like

Once the registrar's nameserver list matches exactly what the recreated zone was assigned, the domain resolves through the correct servers on the next lookup, and a trace from the root shows a clean handoff with no stale names anywhere in the path. Make it a habit: any time a hosted zone is deleted and recreated, check the registrar in the same sitting, before you move on to re-adding records.

FAQ

Why did my domain break after I deleted and recreated the hosted zone?

When you recreate a hosted zone, the DNS provider hands it a brand new, randomly chosen set of nameservers. It does not reuse the old ones. Your registrar still lists the old nameservers from the deleted zone, so the domain keeps trying to use servers that no longer hold your records until you update the registrar by hand.

Can I just edit the zone's own NS records to match the registrar instead?

No. The provider manages the zone's own NS record set automatically and will overwrite or ignore manual edits to it. The fix always goes the other direction: copy the new zone's nameservers and paste them into the registrar's nameserver settings.

How long until the domain works again after I fix the registrar?

Often minutes for the delegation itself to update, but allow up to 24 to 48 hours for full global convergence as resolver caches and TTLs expire everywhere. You can confirm sooner by querying the new nameservers directly and by running a trace from the root.

Related field notes

Citations

On the problem:

  1. AWS re:Post: Troubleshoot NXDOMAIN responses when using Route 53 as the DNS service. repost.aws/knowledge-center/route-53-troubleshoot-nxdomain-responses
  2. Cloudflare docs: Domain deleted from Cloudflare, troubleshooting the delegation gap. developers.cloudflare.com/dns/zone-setups/troubleshooting/domain-deleted
  3. AWS Route 53 Developer Guide: Getting the name servers for a public hosted zone. docs.aws.amazon.com/Route53

On the solution:

  1. AWS re:Post: Successfully update your name servers at the Route 53 registrar. repost.aws/knowledge-center/route-53-update-name-servers-registrar
  2. Cloudflare docs: Update nameservers. developers.cloudflare.com/dns/nameservers/update-nameservers
  3. AWS re:Post: Resolve website access issues in Amazon Route 53. repost.aws/knowledge-center/website-access-route-53

Stuck on a tricky one?

If you have a DNS, delegation, or email authentication 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 broken delegation?

If this saved you a confusing outage or a support ticket you could not explain, 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