DEV Community

NexGenData
NexGenData

Posted on

DNS Propagation: Why 15 Resolvers Matter and How to Check Them All in One Call

DNS Propagation: Why 15 Resolvers Matter and How to Check Them All in One Call

If you've ever shipped a DNS change and then spent the next 45 minutes refreshing whatsmydns.net to watch the green checkmarks tick across a world map, this post is for you.

whatsmydns is a lovely tool. It is also fundamentally manual. You paste a hostname, click "Search," and eyeball a grid of ~20 resolvers to see which ones have picked up your change. There's no API. There's no programmatic way to run the check on a schedule. There's no way to wire the result into your deploy pipeline so the build halts if propagation hasn't completed.

That gap is what the NexGenData DNS Propagation Checker fills. One API call queries 15 globally distributed, operator-distinct DNS resolvers for a given hostname and record type, returns structured JSON, and detects anomalies like Cloudflare's answer rotation and regional CDN inconsistencies.

This post explains which 15 resolvers it hits, why those specific 15, why checking outside of Google/Cloudflare/Quad9 matters more than most engineers realize, and how to wire the actor into real workflows with cURL and Python.

Why DNS Propagation Is a Real Problem (and Not Just Vibes)

"DNS propagation" is one of those phrases that gets used imprecisely. Technically, DNS doesn't "propagate"—authoritative nameservers answer queries directly, and caching resolvers forget old answers when TTLs expire. There's no update being pushed from a central point.

But the experienced reality is different. When you update an A record, users in different parts of the world see the change at different times because:

  1. Different recursive resolvers cache independently. Google's 8.8.8.8 in us-east caches separately from Google's 8.8.8.8 in eu-west. Cloudflare's 1.1.1.1 in Asia has its own cache.
  2. Resolvers honor TTLs unevenly. Some respect your authoritative TTL precisely; others cap at a minimum (typically 60s) or maximum (typically 24h).
  3. Some networks run their own caching layer on top. ISPs often proxy to a public resolver but cache answers themselves. Corporate VPNs do this too.
  4. Anycast routing changes which PoP you hit. A user flying from LA to Tokyo might suddenly hit a different Cloudflare PoP that hasn't pulled the new answer yet.

The net effect: a DNS change that looks "propagated" from your laptop can still be serving stale answers to a double-digit percentage of global traffic for hours. If you flipped that DNS record as part of a blue/green deploy, a CDN migration, or a mail server move, that percentage is your users seeing 404s, broken TLS, or email bouncing.

Why 15 Resolvers, and Specifically These 15

You could check 100 resolvers. Or 5. The number 15 is a balance between two realities:

  1. Most resolvers are downstream of a handful of major upstreams. Thousands of ISP resolvers in the world ultimately proxy to Google, Cloudflare, Quad9, OpenDNS, or Verisign. Checking all of them is redundant.
  2. A small number of operators run their own distinct infrastructure with their own caches and peering arrangements. If you only check the "big three" (Google, Cloudflare, Quad9), you miss those—and those are where real-world users actually hit propagation gaps.

The 15 resolvers the actor checks are chosen to be operator-distinct—each one is run by a different organization, with its own infrastructure, in a different geography:

Resolver Operator Region Notes
8.8.8.8 Google Global anycast Dominant consumer resolver
1.1.1.1 Cloudflare Global anycast Fastest globally on average
9.9.9.9 Quad9 Global anycast Swiss-based, filters malware
208.67.222.222 OpenDNS (Cisco) Global anycast Enterprise / family filter use
64.6.64.6 Verisign US Infrastructure-tier, different peering
77.88.8.8 Yandex Russia Critical for RU/CIS user traffic
94.140.14.14 AdGuard Global DNS-level ad blocking userbase
185.228.168.168 CleanBrowsing Global Family-filter and schools use
156.154.70.1 Neustar/UltraDNS Global Enterprise DNS, different cache
101.101.101.101 TWNIC Quad101 Taiwan / APAC Regional resolver, distinct cache
223.5.5.5 Alibaba China / APAC Major APAC consumer resolver
80.80.80.80 Freenom Netherlands European regional
216.146.35.35 Dyn (Oracle) US Legacy but still in corporate use
8.26.56.26 Comodo Secure DNS Global Security-focused filtering
199.85.126.10 Norton ConnectSafe US Consumer security product

The reasoning for each cluster:

  • Global dominants (Google, Cloudflare, Quad9): What the average consumer hits.
  • Regional critical (Yandex, Alibaba, TWNIC): Where non-Western user traffic actually resolves. Checking only 1.1.1.1 tells you nothing about what users in Moscow or Shanghai are seeing.
  • Enterprise and filtering (OpenDNS, CleanBrowsing, Comodo, Norton, AdGuard): Corporate networks and family-filter setups route through these. A propagation bug here silently breaks a chunk of your B2B customers.
  • Infrastructure-tier (Verisign, Neustar, Dyn): Distinct peering and caching; frequently used as upstream by smaller ISPs.

This coverage is empirically enough to catch all the real-world propagation anomalies I've seen in production—without padding the list with 80 resolvers that are really just downstream of these 15.

What Each Query Actually Returns

For each of the 15 resolvers, per record type requested (A, AAAA, CNAME, MX, TXT, NS, CAA, SOA), the actor returns:

  • The response: the record(s) returned, or NXDOMAIN / SERVFAIL
  • Response time in milliseconds
  • TTL (as served by that resolver)
  • Whether the answer matches the authoritative answer (pulled separately from the domain's NS records)

The "matches authoritative" bit is the one that matters. A resolver returning your old IP is not propagated. A resolver returning the new IP matches. The actor computes a propagationPct across the 15 resolvers so you get a single number you can assert on in CI.

Try It: Single Domain, A Record, cURL

curl "https://api.apify.com/v2/acts/ITdMyAckvJED46a8V/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -X POST \
  -H 'Content-Type: application/json' \
  -d '{
    "domain": "example.com",
    "recordTypes": ["A"],
    "compareToAuthoritative": true
  }'
Enter fullscreen mode Exit fullscreen mode

Response (abridged):

[
  {
    "domain": "example.com",
    "recordType": "A",
    "authoritative": {
      "nameservers": ["a.iana-servers.net", "b.iana-servers.net"],
      "answer": ["23.192.228.80", "23.192.228.84", "23.215.0.136", "23.215.0.138", "96.7.128.175", "96.7.128.198"]
    },
    "resolvers": [
      {
        "resolver": "8.8.8.8",
        "operator": "Google",
        "answer": ["23.192.228.80", "23.192.228.84", "23.215.0.136", "23.215.0.138", "96.7.128.175", "96.7.128.198"],
        "ttl": 3600,
        "responseTimeMs": 14,
        "matchesAuthoritative": true
      },
      {
        "resolver": "1.1.1.1",
        "operator": "Cloudflare",
        "answer": ["23.192.228.80", "23.192.228.84", "23.215.0.136", "23.215.0.138", "96.7.128.175", "96.7.128.198"],
        "ttl": 3596,
        "responseTimeMs": 9,
        "matchesAuthoritative": true
      },
      {
        "resolver": "77.88.8.8",
        "operator": "Yandex",
        "answer": ["23.192.228.80", "23.192.228.84", "23.215.0.136", "23.215.0.138", "96.7.128.175", "96.7.128.198"],
        "ttl": 3200,
        "responseTimeMs": 87,
        "matchesAuthoritative": true
      }
    ],
    "propagationPct": 100,
    "inconsistencies": []
  }
]
Enter fullscreen mode Exit fullscreen mode

The inconsistencies array is where interesting things live. On a freshly changed record, you'll see entries like:

"inconsistencies": [
  {
    "resolver": "156.154.70.1",
    "operator": "Neustar/UltraDNS",
    "returnedAnswer": ["192.0.2.10"],
    "authoritativeAnswer": ["192.0.2.20"],
    "issue": "stale_record",
    "staleTtl": 1243
  }
]
Enter fullscreen mode Exit fullscreen mode

That's telling you: Neustar hasn't picked up your change yet, and has 1,243 seconds of stale TTL left.

Bulk Mode: Assert Propagation in a Deploy Pipeline

The pattern I use most: check propagation after a DNS change as part of a deploy pipeline, and fail the deploy if propagation is below a threshold. Python:

import os
import sys
from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])

DOMAIN = "api.example.com"
THRESHOLD_PCT = 90  # require 90% of resolvers to be current

run = client.actor("nexgendata/dns-propagation-checker").call(run_input={
    "domain": DOMAIN,
    "recordTypes": ["A", "AAAA"],
    "compareToAuthoritative": True,
})

results = list(client.dataset(run["defaultDatasetId"]).iterate_items())

failed = False
for r in results:
    pct = r["propagationPct"]
    rec = r["recordType"]
    print(f"{rec}: {pct}% of 15 resolvers current")
    if pct < THRESHOLD_PCT:
        failed = True
        for inc in r["inconsistencies"]:
            print(f"  Stale on {inc['operator']} ({inc['resolver']}): "
                  f"{inc['returnedAnswer']} vs authoritative {inc['authoritativeAnswer']}")

if failed:
    print(f"\nPropagation below {THRESHOLD_PCT}% threshold. Halting deploy.")
    sys.exit(1)

print("\nPropagation OK. Proceeding.")
Enter fullscreen mode Exit fullscreen mode

Drop that into a GitHub Actions step after your DNS-updating job and it'll halt the deploy until the new record has reached the threshold globally. This is a real thing you want in CI for blue/green deployments where the switchover is DNS-based.

Cloudflare Answer Rotation (and Why Your Checks Lie Without It)

A thing that trips people up: Cloudflare's A records for proxied hostnames return different IPs each query, because the Cloudflare edge picks one of a pool. If you naively compare the answer from resolver A to the answer from resolver B, they'll disagree even though both are fully propagated.

The actor handles this. When it detects that the authoritative answer comes from a Cloudflare nameserver (ns[1-6].cloudflare.com) and the returned IPs are within Cloudflare's published ranges, it relaxes the match criterion to "is this IP in the expected Cloudflare ranges?" rather than "exact match to authoritative."

The same kind of logic applies to:

  • Anycast CDN records (Fastly, Akamai, CloudFront) that rotate per-query
  • DNS-based load balancing (AWS Route 53 weighted/latency records)
  • GeoDNS (different answers by client subnet)

You can toggle this with strictMatch: true if you want exact-string comparison and don't care about the rotation semantics.

JavaScript: On-Demand Check for a Web Dashboard

If you're exposing a "did my DNS change go through" tool inside an internal dashboard:

async function checkPropagation(domain, recordType = "A") {
  const res = await fetch(
    `https://api.apify.com/v2/acts/ITdMyAckvJED46a8V/run-sync-get-dataset-items?token=${process.env.APIFY_TOKEN}`,
    {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        domain,
        recordTypes: [recordType],
        compareToAuthoritative: true,
      }),
    }
  );

  const [result] = await res.json();
  return {
    pct: result.propagationPct,
    current: result.resolvers.filter(r => r.matchesAuthoritative).length,
    total: result.resolvers.length,
    stale: result.inconsistencies.map(inc => ({
      operator: inc.operator,
      ttlRemaining: inc.staleTtl,
    })),
  };
}

// Usage
const status = await checkPropagation("api.example.com", "A");
console.log(`${status.current}/${status.total} resolvers current (${status.pct}%)`);
if (status.stale.length) {
  console.log("Stale on:");
  status.stale.forEach(s => console.log(`  ${s.operator} (TTL ${s.ttlRemaining}s remaining)`));
}
Enter fullscreen mode Exit fullscreen mode

Wire that into a dashboard widget and you've replaced whatsmydns.net with something scriptable.

Real Use Cases I Use This For

1. Blue/Green Deploy Gating

A company I work with does traffic cutover via DNS—old environment on old IPs, new environment on new IPs, flip the A record when ready. Without a propagation check, the "flip" can leave 10% of users on the old environment for hours. With the check running in the deploy pipeline, the pipeline waits until ≥95% propagation before marking the deploy complete.

2. Email Migration Verification

Moving MX records from an old mail host to a new one is a classic "one-resolver-is-stale" story. A 5% stale resolver share means 5% of inbound mail goes to the old host and bounces. The actor's MX record check flags this across all 15 resolvers.

3. Post-Incident DNS Forensics

When something breaks and the incident postmortem involves "was it DNS," the actor's historical results (the actor writes each run to a persistent dataset) let you see the propagation state at the moment of the incident rather than trying to reconstruct it from memory.

4. Geographic Routing Sanity Checks

Launching in a new region (APAC, EMEA) and want to verify your GeoDNS is routing correctly? Check the same domain from Yandex, Alibaba, TWNIC, and Comodo and look at the IPs returned. If all 15 return the same single IP, your GeoDNS isn't working. If regional resolvers return different IPs, it is.

5. Cloudflare / Provider Outage Detection

When Cloudflare's control plane hiccups (it happens), authoritative answers for proxied records go weird. Running a propagation check across 15 resolvers surfaces this as "Cloudflare nameservers returning different answers than other NS" faster than waiting for a status page update.

How Does This Compare to the Alternatives?

Tool API Bulk Resolvers checked Notes
whatsmydns.net No No ~20 Manual, UI-only
dig (one resolver at a time) N/A No 1 The "DIY" baseline
DNSChecker.org No No ~30 UI-only, similar to whatsmydns
Pingdom DNS check Yes Yes Varies Part of $15/mo+ monitoring plans
UptimeRobot Yes Yes Limited Per-resolver check requires Pro
NexGenData Propagation Checker Yes Yes 15 distinct operators ~$0.003 per domain

For the use cases above (CI gating, scheduled checks, dashboards), the API-accessible version wins by default; the only question is which API-accessible version, and the per-domain price + operator-distinctness of this actor makes it the one I actually use.

Answering the Common Questions

Q: Is 15 resolvers really enough?
A: For practical propagation confidence, yes. I tested the actor against 50+ resolvers in development; the marginal information above 15 was essentially zero because the extras were all downstream of the 15 already covered. The operator-distinctness matters more than raw count.

Q: Can I add my own resolvers?
A: Yes. Pass customResolvers: ["1.0.0.1", "my-corporate-resolver.example.com"] in the input and the actor will query those in addition to the built-in 15.

Q: Does it support DNS-over-HTTPS (DoH)?
A: Yes, optionally. useDoh: true will query the resolvers over DoH instead of plain UDP DNS. Slower (a few hundred ms per query) but useful if your egress environment blocks UDP/53.

Q: What about DNSSEC?
A: The actor reports whether each resolver validates DNSSEC for the response (AD flag set in the response). It doesn't do independent DNSSEC validation; for that, use a tool like delv or dnsviz.net.

Q: Can it check a single record type only (like just MX)?
A: Yes. recordTypes: ["MX"]. You can also pass multiple types in one run and get all results back.

Q: How does pricing work?
A: Roughly $0.003 per domain checked, per record type. A single A-record check across 15 resolvers is about $0.003; an A+AAAA+MX+TXT check is four times that. Bulk discounts apply above 1,000 checks per run.

Related NexGenData Actors for a Full DNS/Deliverability Stack

  • Email DMARC Auditor — When you're deploying SPF/DMARC/DKIM changes, pair with propagation checks to confirm both that the record exists and that all resolvers see it.
  • Email RBL Checker — After an IP migration, verify both DNS propagation and that your new IP isn't on any blocklists before cutting over email traffic.
  • Google Cache Viewer — Historical view of a site's content when propagation issues make current DNS unreliable.
  • Website Performance Monitor — Pair with DNS propagation checks to catch "DNS is propagated but origin is slow" vs "DNS is stale" as distinct classes of issue.

Try It

Single domain, A record, right now:

curl "https://api.apify.com/v2/acts/ITdMyAckvJED46a8V/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -X POST \
  -H 'Content-Type: application/json' \
  -d '{"domain": "yourdomain.com", "recordTypes": ["A"]}'
Enter fullscreen mode Exit fullscreen mode

Or from the UI: apify.com/nexgendata/dns-propagation-checker.

Apify's free tier includes $5 of credits. That's enough for roughly 1,500 A-record propagation checks, or a few hundred multi-record checks.


Want more tools like this? I'm a solo dev shipping Apify actors that plug the gaps between "free but manual" and "$150/mo SaaS"—subscribe to the newsletter for new releases.

Build your own actors: sign up at apify.com and ship alongside mine.


Resources:

Top comments (0)