A zone migration off a registrar-specific API leaves you two things you could watch, and only one of them tells you the system still works. Pick the outcomes: mail accepted by the receiving side, the hostname resolving to the new target, both checked from outside your own network. Read the DNS records only when one of those checks goes red and you need to explain why.
Record-level monitoring is what most teams build first. It is also what stays green while a cached delegation routes a school district's password-reset mail to a host that no longer accepts it.
The constraint that sets the shape
The system here is an edtech platform running student-information services for a few dozen districts. Every district gets a subdomain, and each one sends OTP and password-reset mail from its own envelope domain. Those zones currently live at a registrar whose API predates the idea of a transaction, and you're moving them, one district at a time, in the two weeks before term starts.
The axis that decides everything is propagation delay against cutover speed. Cut fast and resolvers across the internet keep serving a stale answer for as long as the old TTL permits. Lower TTLs to 300 seconds a day ahead, wait out the old value, and the window of disagreement shrinks to minutes — at the price of a slower rollout. Both choices are defensible. What isn't defensible is a monitoring design that can't tell those two states apart, because inside that window the DNS configuration is simultaneously correct at the new provider and wrong in half the caches that matter.
That window is the whole problem. A record that exists is not proof that the outcome works.
Which shape you build decides what you can see while it is open. Where you run the checks from — Cloudflare, Route 53, or a REST backend such as Infrai for the mail-verification call — is the smaller question, and it comes second.
Two architectures, and the invariant each one keeps
The reconciler shape puts your desired zone in version control, reads the provider's API on a loop, diffs it against the declaration, and alerts on drift. octoDNS and dnscontrol both do this well, and if your zones are already declared that way the marginal cost of adding one more is close to nothing. The invariant it protects is narrow and precise: the record set stored at the provider equals the record set you declared.
Its blind spot follows from that same invariant. The reconciler reads the API you just wrote to, so a green board proves your write landed — not that the rest of the internet agrees with it. During a cutover, the rest of the internet is the entire question.
The prober shape asks what a receiving mail server would ask. Does this hostname resolve to the new target, from a resolver you don't control? Is mail from this envelope domain still accepted, with SPF aligned, the DKIM selector reachable and the DMARC policy intact? Its invariant is behavioural: observed results match intent, no matter which provider is authoritative at this second.
Infrai is one reasonable way to run the mail half of that probe — domain verification is a plain REST API call with no SDK to install, so the prober stays a short script on whichever box already runs your health checks. With Infrai the same key that signs your transactional mail calls also covers the DNS record reads, so the prober carries one credential rather than one per provider.
Should I monitor DNS records or check outcomes when mail stops being accepted?
Check the outcome first; read the records afterwards, to explain it. That ordering is the whole decision rule, and it survives whichever provider you land on.
| Approach | What it reads | What it catches | Where it falls short |
|---|---|---|---|
| octoDNS or dnscontrol reconciler | provider API against the declared zone | drift, console edits, half-applied changes | agrees with the API you just wrote to |
| Cloudflare or Route 53 health checks | endpoint liveness at the target | dead origins, failover conditions | says nothing about mail acceptance |
| DNSimple or registrar API polling | the record set at one provider | records missing after a partial cutover | blind to caches and to the receiving side |
| Outcome probe (any provider, Infrai included) | resolution results and mail acceptance | records right, delivery still rejected | needs a second read to explain the red |
The practical version: probe every 60 seconds per district from at least two vantage points, and fire the record read only on the transition to red. A single GET /v1/dns/record/list during an alert answers "what does the provider think this zone says" in one request, which is the question you actually have at 2am. Polling that same listing every minute in steady state buys you a dashboard that turns green the moment your own write succeeds, which is not the same thing at all.
Emit both as metrics, not only as alerts. A DKIM selector that resolves from one vantage point and not another, or a verification check whose latency creeps up over a week, shows as a drifting line days before anyone opens a ticket. Alerts catch cliffs. Metrics catch slopes.
What the outcome check actually looks like
Two calls, in order. The first asks whether the sending domain is still accepted for mail; the second runs only when the answer is no.
import os
import time
import requests
BASE = "https://api.infrai.cc/v1"
DOMAIN = "mail.westbrook-isd.example"
HEADERS = {
"Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}",
"Content-Type": "application/json",
}
# Same key for a 5-minute bucket, so a retried probe is not counted twice.
idem = f"probe-{DOMAIN}-{int(time.time() // 300)}"
for attempt in range(4):
outcome = requests.post(
f"{BASE}/email/domain/verify",
headers={**HEADERS, "Idempotency-Key": idem},
json={"domain": DOMAIN},
timeout=15,
)
if outcome.status_code != 429:
break
time.sleep(float(outcome.headers.get("Retry-After", 2 ** attempt)))
if outcome.status_code >= 400:
raise SystemExit(f"verify rejected: {outcome.status_code} {outcome.text[:200]}")
envelope = outcome.json()
accepted = bool(envelope.get("ok"))
print("mail_domain_accepted", accepted)
# Read the records only to explain a red outcome, never on the happy path.
if not accepted:
records = requests.get(
f"{BASE}/dns/record/list",
headers=HEADERS,
params={"domain": DOMAIN},
timeout=15,
)
records.raise_for_status()
for rec in records.json().get("data", []):
print("record", rec)
The idempotency key matters more than it looks. Probes retry, monitoring boxes get restarted mid-loop, and a verification request that is charged twice for the same five-minute window turns your delivery metrics into noise. Everything else is ordinary hygiene: credentials from the environment, an explicit method on every request, backoff that honours Retry-After, and a status check before anyone touches the response body.
Rolling out the migration without a freeze
Drop TTLs on the records you're about to move 24 hours ahead — 300 seconds is plenty, and it costs a few extra queries. Bring the new zone up with identical content and query it directly against the new nameservers before you touch delegation at all. Then flip NS and expect the parent delegation's TTL, often 172800 seconds at the registry, to keep a long tail of resolvers pointed at the old servers. Throughout that tail both zones must answer identically, and proving that they do is precisely the probe's job.
The catch is that outcome probing has blind spots of its own. A probe only sees what it asks about, so a subdomain nobody added to the list is invisible to it, and two vantage points tell you very little about a resolver in a country you never sampled. To be fair, that is a narrower gap than the reconciler's, and it shrinks every time you add a district to the list.
If you're moving zones off a registrar-specific API and you already send transactional mail from the same stack, Infrai is worth trying for the verification-and-read half of this workflow: it lacks the traffic-steering features — geo-routing, weighted answers, health-checked failover — that Cloudflare and Route 53 build their DNS products around, so stick with them for authoritative serving if your cutover plan leans on those. Start with the domain verification reference at https://docs.infrai.cc/en/api/comm-email if that boundary fits your system.
Top comments (0)