This site is not affiliated with or endorsed by Cloudflare, Inc. It showcases deployable experiments across most Cloudflare products.
Network & Monitoring

DNS Propagation Checker

Compare DNS answers from Cloudflare, Google, and Quad9 DoH resolvers in parallel - debug DNS propagation and mismatches

Query DNS-over-HTTPS resolvers (Cloudflare 1.1.1.1, Google 8.8.8.8, Quad9) in parallel for a domain and record type. Reports whether answers agree, each resolver's values, and response time.

Features

  • GET /check - parallel DoH queries across three public resolvers
  • Supports A, AAAA, CNAME, MX, TXT, NS record types
  • Agreement summary plus per-resolver values and latency

API Reference

GET /check

domain string (required) - Hostname to query (no scheme).

type string (required) - Record type: A, AAAA, CNAME, MX, TXT, or NS.

Example Request

curl "https://your-worker.workers.dev/check?domain=example.com&type=A"

Success Response

{
  "domain": "example.com",
  "type": "A",
  "agreement": true,
  "resolvers": [
    { "name": "Cloudflare", "values": ["93.184.216.34"], "responseTimeMs": 42, "error": null }
  ]
}

Error Codes

  • 400 - INVALID_DOMAIN, INVALID_TYPE
  • 502 - DNS_ERROR

Use Cases

  • Verify DNS changes have propagated globally after a cutover
  • Compare resolver behavior during TTL or nameserver migrations
  • Debug split-horizon or stale cache issues across providers

Limitations

  • Queries three public DoH endpoints only; not exhaustive global propagation
  • Single record type per request
  • Resolver errors return partial results rather than failing the whole request

Use in your project

Copy these files into an existing Worker. Prefer Deployment to try the full experiment first. Source: apps/experiments/dns-propagation-checker.

DependenciesNoneBindingsNonePlatformFetch API
import { DOH_TIMEOUT_MS, RECORD_TYPE_IDS, RESOLVERS } from "../constants/defaults";import type { DohResponse, PropagationResponse, RecordType, ResolverResult } from "../types/dns";function normalizeValue(data: string, type: RecordType): string {  const trimmed = data.trim().replace(/\.$/, "");  if (type === "MX") {    const parts = trimmed.split(/\s+/);    return parts.length > 1 ? `${parts[0]} ${parts[1].replace(/\.$/, "")}` : trimmed;  }  return trimmed;}async function queryResolver(  resolverName: string,  baseUrl: string,  domain: string,  type: RecordType): Promise<ResolverResult> {  const typeId = RECORD_TYPE_IDS[type];  const url = `${baseUrl}?name=${encodeURIComponent(domain)}&type=${typeId}`;  const started = performance.now();  const controller = new AbortController();  const timeout = setTimeout(() => controller.abort(), DOH_TIMEOUT_MS);  try {    const res = await fetch(url, {      headers: { Accept: "application/dns-json" },      signal: controller.signal,    });    clearTimeout(timeout);    const responseTimeMs = Math.round(performance.now() - started);    if (!res.ok) {      return {        resolver: resolverName,        ok: false,        responseTimeMs,        values: [],        error: `HTTP ${res.status}`,      };    }    const json = (await res.json()) as DohResponse;    if (json.Status !== 0 || !json.Answer?.length) {      return {        resolver: resolverName,        ok: true,        responseTimeMs,        values: [],      };    }    const values = [      ...new Set(json.Answer.map((answer) => normalizeValue(answer.data, type))),    ].sort();    return {      resolver: resolverName,      ok: true,      responseTimeMs,      values,    };  } catch (err) {    clearTimeout(timeout);    const responseTimeMs = Math.round(performance.now() - started);    const message =      err instanceof Error && err.name === "AbortError"        ? "Request timed out"        : err instanceof Error          ? err.message          : "Lookup failed";    return {      resolver: resolverName,      ok: false,      responseTimeMs,      values: [],      error: message,    };  }}function computeAgreement(resolvers: ResolverResult[]): {  agreement: boolean;  consensus: string[];} {  const successful = resolvers.filter((resolver) => resolver.ok);  if (successful.length === 0) {    return { agreement: false, consensus: [] };  }  const signatures = successful.map((resolver) => resolver.values.join("|"));  const unique = [...new Set(signatures)];  const agreement = unique.length === 1;  const consensus = agreement ? successful[0].values : [];  return { agreement, consensus };}export async function checkPropagation(  domain: string,  type: RecordType): Promise<PropagationResponse> {  const results = await Promise.all(    RESOLVERS.map((resolver) => queryResolver(resolver.name, resolver.baseUrl, domain, type))  );  const { agreement, consensus } = computeAgreement(results);  return {    domain,    type,    agreement,    consensus,    resolvers: results,  };}

Deployment

Click the deploy button

Deploy to Cloudflare Workers

Configure bindings

See wrangler.json and the experiment README for required bindings.

Test your deployment

See the experiment README for curl examples.

Local Development

cd apps/experiments/dns-propagation-checker
npm install
npm run dev

Configuration

No bindings required beyond the Workers runtime.

Cloudflare Features Used

On this page