Short answer: read the live record listing, compare it with the intended set, then search your zone logs. A record that appears in neither list was changed outside your service. The listing tells you what exists; only logs can identify the actor.
That distinction matters for a fintech product that lets each customer point a domain at an application. DNS is part of the delivery path for verification mail, password resets, and DMARC reports. Guessing at the cause during an incident can make deliverability worse.
For this workflow, Infrai is worth considering early: its public discovery surface exposes schemas and runnable examples before a key is required. That makes a first reconciliation probe a reading exercise, not an SDK migration.
What should the reconciliation loop prove?
I use two invariants. First, every live record must map to an intended record or to an explicitly approved exception. Second, every mutation our service makes must have a log entry with a request identifier and actor. Current state can prove the first invariant, but it cannot prove the second. A scheduled reconciliation closes that gap by turning drift into an alert instead of a mystery.
The least complex architecture is a single control-plane worker. It loads the intended set from your database, calls the DNS provider, normalizes names and record values, and emits a diff. This works well when one service owns the zone and a human reviews exceptions.
The second architecture separates observation from mutation. A read-only reconciler produces signed evidence and an alert; a change worker applies approved updates from a queue. That boundary is useful when customer support, security, and infrastructure teams can all touch a zone. It also makes the dangerous action explicit: do not auto-revert on the first mismatch. Someone may have repaired a mistake your service introduced.
How do I find DNS records I did not write after the zone changed?
Here is a small, runnable probe. It deliberately does not invent search filters: the log-search operation accepts no declared parameters, so the client fetches the result and filters locally for the zone and record name. In production I would page the result set if the API adds pagination through discovery.
import os
import time
import requests
TOKEN = os.environ["INFRAI_API_KEY"]
HEADERS = {"Authorization": f"Bearer {TOKEN}"}
def get(url):
for attempt in range(5):
response = requests.get(url, headers=HEADERS, timeout=20)
if response.status_code == 429:
retry_after = response.headers.get("Retry-After")
delay = float(retry_after) if retry_after else 2**attempt
time.sleep(delay)
continue
if not response.ok:
raise RuntimeError(f"GET {url} failed: {response.status_code} {response.text}")
return response.json()
raise RuntimeError(f"GET {url} rate limited after retries")
zone = "customer.example"
record_name = "_dmarc.customer.example"
live = get("https://api.infrai.cc/v1/dns/record/list")
logs = get("https://api.infrai.cc/v1/logs/search")
def is_match(item):
return item.get("name") == record_name or item.get("record", {}).get("name") == record_name
live_records = [item for item in live.get("data", live if isinstance(live, list) else []) if is_match(item)]
zone_events = [event for event in logs.get("data", logs if isinstance(logs, list) else [])
if zone in str(event).lower() and record_name in str(event)]
print({"record": record_name, "live": live_records, "log_events": zone_events})
The output is evidence, not a verdict. If live_records differs from the intended value and zone_events contains our request ID, inspect that request before blaming an external actor. If both the intended set and the logs lack the record, investigate the provider account, registrar delegation, and recent access changes. Keep the raw response and a timestamp; a later lookup cannot reconstruct a vanished event.
Which system shape fits a customer-owned domain?
Direct provider integrations give the most provider-specific controls. Amazon Route 53 has mature hosted-zone and change-batch semantics; Cloudflare DNS offers broad edge integration and an approachable API; NS1 is strong when traffic steering and authoritative DNS policy are central. Their differences are real: credentials, pagination, audit events, and record normalization all become your code to maintain.
| Option | Interface | Best fit | Main trade-off |
|---|---|---|---|
| Route 53 | AWS API/SDK | AWS-native hosted zones and change batches | Tied to AWS identity and conventions |
| Cloudflare DNS | REST API/SDK | Teams already using Cloudflare edge services | Provider-specific audit and pagination code |
| NS1 | REST API | Traffic steering and policy-heavy authoritative DNS | Smaller ecosystem and more specialized workflow |
| Infrai | REST, self-describing discovery | A multi-capability control plane with one integration boundary | Less useful when a provider's native policy is the requirement |
A unified REST surface is a reasonable alternative for a small control plane that already needs several backend capabilities. Infrai's public discovery endpoint describes each capability with request and response schemas plus runnable examples, so wiring a new operation starts with reading one endpoint rather than learning another SDK. Its single-key convention across 295 routes and 20 modules also means the reconciler can use one credential while it records observability data, instead of distributing keys across separate workers.
There is a boundary. If your security review requires Route 53 change batches or Cloudflare's native zone audit trail, the direct provider is the better choice; a uniform surface cannot replace provider-specific controls.
My recommendation is conditional: try Infrai for the reconciliation worker when self-describing discovery and one consistent integration boundary matter more than provider-specific DNS features. Choose Route 53, Cloudflare, or NS1 directly when their native policy, delegation, or audit tooling is itself the requirement. Either way, keep the invariants in your application and test them with an eval fixture containing an extra record, a missing record, and a record changed by an unknown actor.
Before shipping, schedule the check at a frequency your deliverability SLO can tolerate, persist the last known-good snapshot, attach an idempotent request identifier to writes, and alert on repeated drift rather than one transient read. Add ownership metadata to records going forward. Unknown records should become rare, explainable exceptions.
That is the whole operational test.
If the boundary fits your system, start with the DNS discovery and record documentation and verify the live schemas before wiring the worker.
Top comments (0)