How to detect DNS record changes

How to detect DNS record changes

5 min read

To detect DNS record changes, you snapshot the records you care about, store the result, and re-query on a schedule, comparing each fresh answer against the stored baseline. The SOA serial is the cheapest signal that something in the zone moved, and a per-record diff tells you exactly what. That loop is ten lines of shell. The reason DNS change detection has a reputation for being noisy is everything after it: telling a real edit apart from GeoDNS, round-robin, and a CDN rotating its address space.

This is the deep version. For what DNS monitoring catches and where it structurally falls short, the wider picture is in what DNS monitoring catches and what it can't. Here the focus is narrow: building change detection that fires on the edits you care about and stays quiet for the ones you do not.

The basic loop: snapshot, re-query, diff

Every DNS change detector, hand-rolled or hosted, is the same three steps: capture a baseline, query again later, compare. The query tool is dig. Pin an explicit resolver so cron and your shell agree, ask for the record types you track, and keep the output stable enough to diff cleanly.

#!/usr/bin/env bash
domain=example.com
resolver=1.1.1.1
for type in A AAAA MX NS TXT CAA; do
  echo "; $type"
  dig @"$resolver" +short "$domain" "$type" | sort
done

Write that to a file as your baseline, run it again on the next tick, and diff the two:

dns-snapshot.sh > current.txt
diff baseline.txt current.txt && echo "no change" || echo "zone changed"

The sort matters. Without it, two identical record sets returned in a different order read as a change, which is the first false positive you will hit. That is the thread the rest of this pulls on.

Use the SOA serial as your cheap trip-wire

Querying every record type on every domain on a tight interval gets expensive fast. The SOA serial collapses that into one query. The serial is a number the zone's primary is meant to increment on every edit, and most providers encode it as a date plus a daily counter:

dig @1.1.1.1 +short example.com SOA
ns1.example.com. hostmaster.example.com. 2026062801 7200 3600 1209600 3600

The third field, 2026062801, is the serial. Poll only the SOA on the fast loop. When the serial moves, run the full per-record snapshot to find what changed. When it does not, you skip the expensive part entirely.

The caveat is that the serial is a convention, not a guarantee. Hidden-primary setups, some managed DNS providers, and a few anycast networks do not bump it the way you expect, and a handful rewrite it on a timer whether or not anything changed. Treat a moved serial as a reliable "look closer" and a still serial as a weak "probably fine", never as proof either way.

Which records to actually watch

Not every record deserves an alert. Watch the ones whose change means traffic, mail, or trust moved:

Record Why a change matters Alert priority
A / AAAA where your name points; a wrong value is a hijack or a broken deploy high
MX where mail is delivered; a wrong value silently reroutes email high
NS delegation itself; a change can hand your whole zone to another operator high
CAA which CAs may issue your certificates; a change enables misissuance high
TXT (SPF / DKIM / DMARC) mail authentication; edits break deliverability quietly medium
CNAME aliasing; a dangling target invites subdomain takeover medium

DS records belong on that list too if you run DNSSEC, because a bad DS at the parent breaks validation for the whole zone. The point is to rank, not to watch everything equally. An alert on every TTL tweak trains you to ignore the channel.

The false positives that make people give up

Here is why most naive detectors get muted inside a week. A lot of legitimate, healthy DNS looks like a change to a dumb diff.

Round-robin and record order. A domain with several A records hands them back in a rotating order to spread load. The set is identical, the order is not. Compare sorted sets, never raw output, or every single query looks like a change.

GeoDNS and split-horizon answers. Authoritative servers can return a different answer based on where the query came from. A check running in Frankfurt and a check running in Virginia get different, both-correct A records. Detection has to be per-vantage-point: compare each location against its own baseline, not against the others.

CDN address rotation. If your name is CNAMEd to a CDN such as Cloudflare or Fastly, the resolved A records churn constantly and carry no meaning for you. Watch the CNAME target, not the address it currently resolves to. Alerting on the A record of a CDN-fronted host is pure noise.

TTL churn and propagation lag. Query during a propagation window, or across resolvers with different cache states, and you catch inconsistent answers that settle on their own. Query the authoritative nameservers directly for change detection, and keep the resolver-level view as a separate, more forgiving check.

Strip those four out and the diffs that remain are almost all real. Leave them in and you will eventually turn the alert off, which is worse than never having built it.

SERVFAIL vs NXDOMAIN vs empty answer

A detector also has to read failures correctly, because they are not all the same and they are not all a deletion.

  • NXDOMAIN: the name does not exist. For a record you expect to be there, that is a real, high-severity change, usually a deleted record or a lapsed delegation.
  • SERVFAIL: the resolver could not complete the lookup. Often transient, a DNSSEC validation failure, or an unreachable authoritative server. Do not read a single SERVFAIL as a deletion.
  • NOERROR with no answer: the name exists but holds no record of that type. Different from NXDOMAIN, and perfectly normal for, say, an AAAA query against an IPv4-only host.

Confirm a failure before you act on it. One SERVFAIL during a re-query is noise. The same SERVFAIL from three resolvers, thirty seconds apart, is a signal.

Alerting without crying wolf

A real DNS change is durable, so a detector can afford to verify before it pages. Two cheap rules remove most of the remaining noise. Require a change to survive a second look a short interval later, and compare against the authoritative answer rather than whatever a public resolver happened to have cached. A genuine edit survives both checks. Propagation flapping does not.

Then route by severity instead of firing one undifferentiated "something changed". A changed NS or CAA record earns a page. A new vendor verification TXT record earns a log line and a place in the daily digest. Treating both as the same alert is exactly how the alert gets turned off for good.

That confirm-before-alert discipline is the same one that keeps uptime checks from paging on every transient blip, worked through in how often you should check uptime.

When to stop hand-rolling

A snapshot-and-diff script is the right tool for a handful of domains. It stops being the right tool when the operational tail catches up: dozens or hundreds of zones, history you can audit after an incident, DNSSEC validated properly rather than by grepping a flag, and answers checked from more than one network so GeoDNS does not blind you. Each of those is buildable. Together they are a product you now maintain instead of doing your actual job.

WebPixie's DNS monitoring is that loop run for you: daily checks across more than 20 record types from several public resolvers (Google, Cloudflare, and Quad9), DNSSEC validation, SPF, DMARC, and BIMI parsing, and change alerts, with the diffing and the false-positive handling already accounted for. The daily cadence is the same tradeoff as your cron job, swapped for not maintaining it.

Either way the method does not change. Snapshot, watch the serial, diff the records that matter, and spend your real effort on the false positives, because that is the part that decides whether anyone still trusts the alert a month from now.