Detect and Monitor DNS Changes¶
DNS changes — whether planned (a migration) or unplanned (a hijack or misconfiguration) — can take time to propagate and are easy to miss. The DNS Toolkit gives you the tools to snapshot records, compare resolvers for inconsistencies, verify propagation, and detect drift.
For full parameter and response schema details, see the API reference →
Two monitoring patterns¶
Post-deployment verification — you made a DNS change and want to confirm it's propagated correctly everywhere.
Ongoing drift detection — you run scheduled checks to catch unexpected changes (hijacks, misconfigurations, TTL drift).
Post-deployment verification¶
After making a DNS change, use propagation to check how far it's spread across public resolvers, and compare-resolvers to detect any that are returning stale or inconsistent answers.
# pip install toolkitapi
from toolkitapi import DNS
domain = "yourdomain.com"
record_type = "A"
with DNS(api_key="YOUR_KEY") as dns:
# Check propagation across global resolvers
prop = dns.propagation(domain, type=record_type)
resolved = [r for r in prop.resolvers if r.status == "resolved"]
print(f"Propagated to {len(resolved)}/{len(prop.resolvers)} resolvers")
for r in prop.resolvers:
if r.status != "resolved":
print(f" ⚠ {r.location}: {r.status}")
# Compare resolver answers for inconsistencies
compare = dns.compare_resolvers(domain, type=record_type)
if compare.consistent:
print("All resolvers agree ✓")
else:
print("Resolver disagreement detected:")
for group in compare.groups:
print(f" {group.value} → seen by: {group.resolvers}")
Ongoing drift detection¶
Take a full DNS snapshot, store it, and compare on a schedule. Alert if anything changes.
import json
from datetime import datetime
from toolkitapi import DNS
def snapshot_dns(domain: str, api_key: str) -> dict:
"""Get all DNS records and return as a comparable dict."""
with DNS(api_key=api_key) as dns:
result = dns.lookup_all(domain)
snapshot = {}
for group in result.results:
# Sort values so order changes don't trigger false positives
snapshot[group.type] = sorted(
[(r.value, r.ttl) for r in group.records]
)
return snapshot
def detect_changes(baseline: dict, current: dict) -> list[str]:
changes = []
all_types = set(baseline) | set(current)
for rtype in all_types:
if baseline.get(rtype) != current.get(rtype):
changes.append(f"{rtype} changed: {baseline.get(rtype)} → {current.get(rtype)}")
return changes
API_KEY = "YOUR_KEY"
domain = "yourdomain.com"
# First run: save baseline
baseline = snapshot_dns(domain, API_KEY)
with open(f"{domain}_baseline.json", "w") as f:
json.dump({"timestamp": datetime.utcnow().isoformat(), "records": baseline}, f)
print(f"Baseline saved at {datetime.utcnow().isoformat()}")
# Subsequent runs: compare against baseline
with open(f"{domain}_baseline.json") as f:
saved = json.load(f)
current = snapshot_dns(domain, API_KEY)
changes = detect_changes(saved["records"], current)
if changes:
print(f"⚠ DNS changes detected for {domain}:")
for c in changes:
print(f" - {c}")
else:
print(f"✓ No DNS changes for {domain}")
Step 1 — Propagation check¶
propagation queries the domain from ~20 public resolvers worldwide and reports the status from each. Useful immediately after making a change to confirm it's spreading.
from toolkitapi import DNS
with DNS(api_key="YOUR_KEY") as dns:
result = dns.propagation("yourdomain.com", type="A")
for resolver in result.resolvers:
status = "✓" if resolver.status == "resolved" else "⚠"
print(f"{status} {resolver.location}: {resolver.value or resolver.status}")
curl "https://dns.toolkitapi.io/v1/propagation?domain=yourdomain.com&type=A" \
-H "X-API-Key: YOUR_KEY"
const params = new URLSearchParams({ domain: "yourdomain.com", type: "A" });
const r = await fetch(`https://dns.toolkitapi.io/v1/propagation?${params}`, {
headers: { "X-API-Key": "YOUR_KEY" },
});
const data = await r.json();
data.resolvers.forEach(r => console.log(`${r.location}: ${r.status} ${r.value || ""}`))
Step 2 — Compare resolvers¶
compare-resolvers queries the same record from multiple resolvers and groups the answers. If resolvers disagree, it means some are returning stale cached records (split-brain) — a common problem during migrations or after changes with long TTLs.
from toolkitapi import DNS
with DNS(api_key="YOUR_KEY") as dns:
result = dns.compare_resolvers("yourdomain.com", type="A")
if result.consistent:
print("All resolvers agree")
else:
print("Inconsistency detected:")
for group in result.groups:
print(f" {group.value} ← {', '.join(group.resolvers)}")
curl "https://dns.toolkitapi.io/v1/compare-resolvers?domain=yourdomain.com&type=A" \
-H "X-API-Key: YOUR_KEY"
const params = new URLSearchParams({ domain: "yourdomain.com", type: "A" });
const r = await fetch(`https://dns.toolkitapi.io/v1/compare-resolvers?${params}`, {
headers: { "X-API-Key": "YOUR_KEY" },
});
const data = await r.json();
console.log(`Consistent: ${data.consistent}`);
Step 3 — Full record snapshot¶
lookup-all retrieves all common record types (A, AAAA, MX, TXT, NS, SOA, CNAME) in a single call. Use this to take a complete baseline snapshot for drift detection.
from toolkitapi import DNS
with DNS(api_key="YOUR_KEY") as dns:
result = dns.lookup_all("yourdomain.com")
for group in result.results:
print(f"\n{group.type}:")
for record in group.records:
print(f" {record.value} (TTL {record.ttl}s)")
curl "https://dns.toolkitapi.io/v1/lookup/all?domain=yourdomain.com" \
-H "X-API-Key: YOUR_KEY"
const params = new URLSearchParams({ domain: "yourdomain.com" });
const r = await fetch(`https://dns.toolkitapi.io/v1/lookup/all?${params}`, {
headers: { "X-API-Key": "YOUR_KEY" },
});
const data = await r.json();
data.results.forEach(g => {
console.log(`\n${g.type}:`);
g.records.forEach(rec => console.log(` ${rec.value} TTL ${rec.ttl}`));
});
Step 4 — Bulk lookup for multiple domains¶
If you're monitoring a fleet of domains (e.g. customer domains on a SaaS platform), lookup/bulk resolves multiple domains in parallel.
from toolkitapi import DNS
domains = ["customer1.com", "customer2.io", "customer3.co"]
with DNS(api_key="YOUR_KEY") as dns:
result = dns.lookup_bulk(domains, type="A")
for entry in result.results:
values = [r.value for r in entry.records]
print(f"{entry.domain}: {values}")
curl -X POST "https://dns.toolkitapi.io/v1/lookup/bulk" \
-H "X-API-Key: YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"domains": ["customer1.com", "customer2.io"], "type": "A"}'
const r = await fetch("https://dns.toolkitapi.io/v1/lookup/bulk", {
method: "POST",
headers: {
"X-API-Key": "YOUR_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({ domains: ["customer1.com", "customer2.io"], type: "A" }),
});
const data = await r.json();
data.results.forEach(e => console.log(`${e.domain}: ${e.records.map(r => r.value)}`))
Scheduling tips¶
- Use a cron job or your task scheduler to run drift detection on a regular interval (hourly for critical domains, daily for the rest).
- Store baselines in a simple JSON file or your database — the record structure is stable.
- Alert on any change to A, MX, or NS records; TXT record changes (especially SPF/DMARC) are also high-value signals.
- Compare-resolvers is best run immediately after a change — not on a slow schedule — because resolver caches expire quickly.