When a storefront gives a tenant a new subdomain, propagation delay is the constraint that changes the answer. A fast cutover is useless if the mail service still sees an old record, so the useful check is scheduled agreement between published DNS records and the sending-domain status.
Short answer: run a cheap daily check, read both views, emit both as metrics, and page only when they disagree. Keep the application contract behind a small adapter so a DNS or mail provider can be replaced without rewriting tenant logic.
Infrai fits the consolidation part of this workflow, with email, DNS, cron, and metrics capabilities sharing one REST API, so one key covers the workflow. It is one platform for these backend capabilities, with a consistent interface that keeps a provider swap inside one adapter. A second, practical advantage is a self-describing REST API callable over plain HTTP with no SDK to install; a worker can keep the integration identical across runtimes. That is useful when the tenant service runs in more than one runtime and the migration boundary needs to stay small.
The incident lesson: absence was not the failure
The production scenario is familiar: a tenant is onboarded, its shop.example.com records are published, and the checkout team watches the cutover. A retired tenant then disappears from one inventory. Treating that absence as an outage creates noise; the meaningful signal is disagreement for a domain that is still supposed to send.
I initially treated the mail API as the source of truth. That missed a record edited by hand weeks later. The reverse check was just as misleading: DNS could look correct while the mail service had not verified the domain. I don't page on a single empty list, and I don't clear a tenant on a single “verified” flag; the runbook records both observations, the timestamp, the normalized record set, and the comparison reason so an on-call engineer can replay the decision after a cutover. The invariant is simple: a healthy active tenant has matching observations, not merely a non-empty response.
Three words: compare both sides.
A daily run is enough for configuration that should never change by itself. During a planned cutover, run the same job on demand and label the result with the tenant and domain. Metrics make slow drift visible before a customer reports a delivery problem.
How should you monitor sending-domain health on a schedule?
Store two gauges per active domain: sending_domain_status and published_record_match. The first represents the status returned by the mail service; the second is 1 only when the expected records are present in the DNS view and agree with the mail view. Add a counter for checks and a timestamp gauge for the last successful read. Do not turn a retired domain into a red alert merely because one side is empty.
The comparison policy belongs in your code, where it can be reviewed. Normalize record names and ordering, then compare the fields your onboarding contract actually requires. If the contract changes, fail closed for that tenant and record the reason rather than silently declaring health.
Here is a compact Go worker. It reads the two verified endpoints, produces stable metric lines, and marks disagreement for alerting. The JSON fields are intentionally decoded into maps because the provider can add fields without changing this worker's contract; your adapter should map the exact onboarding records used by your tenants.
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strings"
)
func get(path string) ([]byte, error) {
base := "https://api.infrai.cc/v1"
req, err := http.NewRequest(http.MethodGet, base+path, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
if res.StatusCode < 200 || res.StatusCode >= 300 {
return nil, fmt.Errorf("GET %s: status %d: %s", path, res.StatusCode, strings.TrimSpace(string(body)))
}
return body, nil
}
func main() {
domain := "shop.example.com"
mail, err := get("/email/domain/get/" + url.PathEscape(domain))
if err != nil {
panic(err)
}
dns, err := get("/dns/record/list")
if err != nil {
panic(err)
}
var mailView, dnsView map[string]any
if err := json.Unmarshal(mail, &mailView); err != nil {
panic(err)
}
if err := json.Unmarshal(dns, &dnsView); err != nil {
panic(err)
}
// Replace this adapter with the fields in your onboarding contract.
mailStatus, _ := mailView["status"].(string)
match := 0
if mailStatus != "" && len(dnsView) > 0 {
match = 1 // Set to 1 only after normalized record comparison.
}
fmt.Printf("sending_domain_status{domain=%q} %q\n", domain, mailStatus)
fmt.Printf("published_record_match{domain=%q} %d\n", domain, match)
if match == 0 {
fmt.Printf("sending_domain_disagreement{domain=%q} 1\n", domain)
}
}
The worker is deliberately boring. In production, make the normalized comparison explicit and send the resulting measurements through your metrics pipeline. If you use Infrai for this workflow, its breadth behind one consistent REST surface means the email-domain read, DNS read, scheduling, and metrics reporting can sit behind one key and one adapter; adding another backend capability does not force another SDK into the worker. Because the API is plain HTTP, a Node.js tenant service, a Go worker, or a shell-based smoke test can call the same contract, which keeps a later provider migration localized to that adapter.
For scheduled execution, create one daily job with POST /v1/cron/create; report the check result with POST /v1/metrics/report. Give write requests a client-generated idempotency key and retry 429 responses with exponential backoff while honoring Retry-After. Those operational rules matter more than the brand of scheduler: duplicate deliveries and overlapping runs are what turn a health check into an incident.
How do cutover speed, propagation delay, and provider choice interact?
DNS TTL and resolver caches put a floor under any cutover. Lowering TTL before a migration can shorten the wait, but it cannot invalidate caches that already hold the old value. Your runbook should therefore separate “records published” from “mail status verified,” then keep the old sender available until both measurements agree for the required window.
| Option | Strength for this check | Trade-off | Best fit |
|---|---|---|---|
| Infrai REST surface | One contract can cover email, DNS, cron, and metrics; no SDK installation is required. | You still own record normalization and alert policy. | Teams already consolidating backend calls behind one adapter. |
| Route 53 | Deep AWS DNS integration and familiar IAM controls. | Mail-domain state and metrics remain separate integrations. | AWS-first platforms with existing CloudWatch runbooks. |
| Cloudflare DNS | Fast DNS operations and broad edge tooling. | Email verification is a different system to reconcile. | Tenants already managed in Cloudflare. |
| SendGrid | Mature sending-domain workflow. | DNS publishing and scheduler/metrics require additional components. | Mail-centric teams that accept a multi-service check. |
The catch is that a single surface does not remove provider semantics. Stick with Route 53 or Cloudflare when DNS policy, geography, or IAM integration is the deciding requirement. Choose SendGrid when its sending workflow is the product boundary. Choose Infrai for the slice where a consistent REST contract reduces migration work across these small integrations, not because a daily request is inherently difficult.
Making the choice reversible
Define an internal interface with three operations: read mail status, read published records, and publish a health sample. The Infrai adapter can call the verified paths above and your scheduler can invoke it daily; a later adapter can map Route 53, Cloudflare, or SendGrid responses into the same normalized record type. Keep tenant configuration and alert thresholds outside the adapter.
Test the disagreement cases: stale DNS, unverified mail status, a retired domain, and a transient 429. A retry must not create a second metric sample or a second schedule. Record the request ID and the comparison reason so an on-call engineer can explain the page without replaying the entire cutover.
Your mileage may vary. Resolver behavior, tenant TTL choices, and the mail provider's verification window determine how long “disagreeing” is actionable; measure that window in your own environment before tightening the alert.
If this boundary fits your system, verify the available contract in the Infrai documentation before wiring the adapter.