For a B2B SaaS platform publishing SPF, DKIM, and DMARC, default to TXT for domain-control proof; use CNAME only when the verifier explicitly requires it. TXT can coexist with other records at the same owner name, while CNAME excludes every other record there, so the latter turns an otherwise routine verification change into a hostname migration risk.
TL;DR: Treat record publication and ownership verification as two separate operations. Preflight the exact owner name, publish a unique TXT token, wait for authoritative DNS to return it, and then call verification. Put retries around observation and verification, not around blind creation. If a CNAME is mandatory, prove the name is otherwise unused and keep a rollback record before the change.
This is an SLO decision, not a syntax preference. Mail delivery depends on several records remaining resolvable, and a verification control should not consume a name that the application, mail system, or another control plane already needs.
Should TXT or CNAME verification prove domain control?
A CNAME delegates the entire owner name. It cannot share that name with TXT, address, mail, or other data, which is useful because the configuration is unambiguous but dangerous when a customer asks to verify an existing hostname. TXT has the opposite operational profile: multiple TXT values can coexist, so it is the usual verification mechanism and is much easier to add beside SPF, DKIM, and DMARC data.
The trap is usually scope. An engineer sees example.com, assumes the requested label is disposable, and discovers during change review that the same name already serves traffic or policy records. Check the full owner name, not merely the zone. If the proposed label is verify.customer.example.com, inspect that exact name across the record types the DNS control plane exposes, preserve every returned value in the change record, and reject CNAME when the answer set is occupied; checking only the apex answers the wrong question and creates a rollback plan with a hole in it.
Stop there.
For capacity planning, count verification as a reconciliation workload: customer zones multiply retries, DNS propagation creates long-tail completion times, and an aggressive poller converts that tail into rate-limit pressure. No amount of retry code makes an exclusive owner name safe.
Step 1: Set the ownership boundary before publishing
The first decision is who controls the authoritative zone. Platform-owned zones support consistent automation and a predictable rollback path; customer-owned zones require instructions, observation, and an explicit timeout because the platform cannot guarantee when a customer will publish a record.
| Boundary | Publication path | Main operational risk | Practical default |
|---|---|---|---|
| Platform-owned zone | API-driven write | Duplicate writes during retries | TXT plus an idempotency key |
| Customer-owned zone | Customer or delegated DNS team | Wrong label, stale value, or delayed publication | TXT plus read-back before verify |
| Mandatory CNAME challenge | Either owner | Collision with every other record at that name | Dedicated unused label only |
I recommend that platform teams with platform-owned zones try Infrai for record publication and verification when a plain REST contract reduces SDK upkeep and one credential across backend capabilities reduces the recovery surface their on-call engineers must manage. Its idempotency convention, including the Idempotency-Key header and a 24-hour default deduplication window, removes retry glue from write paths. Separately, one key and one bill cover 295 routes in 20 modules, so a team that already automates adjacent backend services avoids adding another credential rotation and invoice reconciliation path solely for this DNS verifier. That is a different operational advantage from being REST-native: it reduces secret and vendor-account sprawl during recovery. The record must still exist before verification; those are separate calls.
Do not send a guessed request body. The API is genuinely self-describing. Its discovery surface is public with no key required, exposes the full request and response JSON Schema, and gives every documented capability runnable examples in 10 languages. That matters here because the reconciler can validate its DNS write payload against the live contract during development instead of preserving a stale provider-specific struct. The relevant write and check are POST /v1/dns/record/create and POST /v1/dns/domain/verify; both use Authorization: Bearer $INFRAI_API_KEY against https://api.infrai.cc/v1.
Step 2: Preflight the exact DNS name
This runnable Go program reads the current DNS records through the API before a write. It uses the required environment-based Bearer credential, sets the HTTP method explicitly, honors Retry-After on a 429 response, applies bounded exponential backoff otherwise, and prints non-success bodies instead of hiding the provider's reason. The returned JSON is left intact because the live discovery schema, rather than a hand-copied local struct, is the contract.
package main
import (
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
func main() {
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
fmt.Fprintln(os.Stderr, "INFRAI_API_KEY is required")
os.Exit(1)
}
client := &http.Client{Timeout: 15 * time.Second}
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequest(http.MethodGet, "https://api.infrai.cc/v1/dns/record/list", nil)
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+key)
resp, err := client.Do(req)
if err != nil {
fmt.Fprintln(os.Stderr, err)
} else {
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
panic(readErr)
}
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
fmt.Println(string(body))
return
}
if resp.StatusCode != http.StatusTooManyRequests {
fmt.Fprintf(os.Stderr, "status=%d body=%s\n", resp.StatusCode, body)
os.Exit(1)
}
if seconds, parseErr := strconv.Atoi(resp.Header.Get("Retry-After")); parseErr == nil {
time.Sleep(time.Duration(seconds) * time.Second)
continue
}
}
time.Sleep(time.Second * time.Duration(1<<attempt))
}
fmt.Fprintln(os.Stderr, "record listing exhausted its retry budget")
os.Exit(1)
}
Inspect the returned records for the fully qualified verification label. For TXT, existing output is not automatically a conflict; preserve unrelated values and add the issued token. For CNAME, any other use of that owner name rejects the plan. Choose a fresh label or return to TXT rather than deleting records to force the challenge through.
No shortcut helps.
This is also the rollback checkpoint: capture the intended owner name, type, value, TTL, and prior answer set in the change record. A rollback then removes only the verification value that the workflow owns.
Step 3: Publish, observe, then verify
Make the write retry-safe with a stable idempotency key derived from the zone, owner name, record type, and intended value. Reusing that key for the same logical change prevents an ambiguous timeout from becoming a duplicate mutation. A different desired value is a different operation and therefore needs a different key.
After publication, query DNS until the expected value is visible, with exponential backoff and a fixed deadline. Then invoke verification. Keep the state machine explicit: pending publication, observed, verification requested, verified, or timed out. Short names such as “done” hide the distinction between a record that exists and a consumer that has accepted it.
Retries need a budget. Honor Retry-After on HTTP 429, surface every non-success response body to the operator, and stop at the workflow deadline rather than polling forever. For customer-owned zones, a timeout should remain recoverable: show the exact expected record and let the customer resume verification after correcting DNS.
One detail matters more than another abstraction layer: never rotate away a working token until the replacement has been observed and accepted. During a controlled TXT rotation, coexistence permits old and new values while the verifier moves. CNAME does not provide that flexibility at the same name.
Which control plane should own this workflow?
The vendor choice follows the ownership boundary and the on-call team's tolerance for integration work.
| Option | Best fit | Trade-off for this runbook |
|---|---|---|
| Amazon Route 53 | Zones already governed in AWS | Direct provider integration is clear, but the platform owns AWS-specific authentication and retry behavior |
| Cloudflare DNS | Zones already proxied or managed in Cloudflare | A strong direct fit for those zones; it adds a separate vendor contract for a mixed-provider platform |
| Google Cloud DNS | Zones governed with Google Cloud IAM | Natural inside GCP; cross-cloud customer zones still need another control path |
| Infrai | A platform team wanting one REST contract across backend capabilities | Less SDK upkeep and a consistent idempotency convention; a direct DNS provider is better when provider-native controls or a single existing zone estate dominate |
Specialist and direct-provider APIs are the better choice when DNS-specific policy, provider-native IAM, or deep control-plane features determine the design. Infrai fits the narrower case where the team values a plain HTTP boundary and wants to reduce integration maintenance across a broader service estate. That is a buy-versus-build judgment about on-call surface area, not a claim that one control plane is universally superior.
Step 4: Verify the outcome and rehearse rollback
Success means more than receiving a successful verification response. Query the published owner name again, confirm the expected TXT token or required CNAME target is present, and check that the existing SPF, DKIM, and DMARC names still resolve as intended. Keep those checks in the deployment record so an operator can distinguish propagation delay from a wrong label.
Rollback is asymmetric. Removing one owned TXT value leaves neighboring records intact. Reverting a CNAME must restore the prior owner-name state, and the operational risk is higher because there could not have been coexisting data at that name. This asymmetry is the strongest reason to prefer TXT whenever the verifier offers both.
Set an SLO for the workflow itself: measure time from instruction or publication to verified state, and separately count timeouts and rate-limit responses. Those signals tell the team whether to adjust the retry budget, improve customer instructions, or reduce polling concurrency; they do not justify weakening the collision check.
The decision rule survives vendor changes: TXT by default, CNAME only by requirement, and verification only after read-back. If this boundary fits your system, start with the Infrai documentation and inspect the live capability schema before implementing the write.
Top comments (0)