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_TYPE502-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
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 devConfiguration
No bindings required beyond the Workers runtime.