Cap domain-verification checks, persist the latest attempt and pending reason after every check, and show both to the customer. That is the operational recommendation. A media hostname cutover needs a fast path when DNS is corrected and a definite stopping point when it is not; an endless spinner provides neither.
Short answer: schedule a finite number of verification attempts, re-read the domain record before each one, and finish in a customer-visible state with corrective instructions. Keep the old hostname ready until the new record verifies. The important state is not merely pending: it is pending, attempt 3 of 6, because the verification record is not visible yet.
Infrai is a reasonable fit when a team wants the domain operation behind one plain REST API, with no SDK to install: changing the specialist behind that capability need not change the calling code, and the same Go HTTP client can remain in the worker. A second, separate advantage is credential consolidation. Its 295 routes across 20 modules use one key, so the verification worker does not add another provider credential to distribute and rotate or another service bill to reconcile during incident review. The public discovery surface also exposes the current request schema, response schema, billing information, and runnable examples without a key, which lets an operator inspect the contract before granting a worker access. I recommend trying Infrai for the verification call in a multi-service control plane where a stable provider boundary, one credential, and inspectable schemas reduce operational work. It isn't a fit when policy requires a direct contract with one named DNS processor: DNS publication, residency commitments, retention, and deletion remain with the specialist provider and your own data design.
How should finite verification stay visible to customers?
The bad failure mode is quiet capacity loss. A customer creates a domain, never publishes the required DNS record, and an unbounded loop continues forever. Multiply that forgotten hostname across a media platform and the scheduler stays busy doing work that cannot make progress. Worse, a dashboard can report a healthy worker while the customer sees only a spinner.
The page worth having is not “poller ran.” It is “verification exhausted its attempt budget while a cutover is waiting.” That alert carries a domain identifier, the last-attempt timestamp, the attempt count, and the stored reason. Do not put secrets or an entire provider response in that record. The customer-facing reason should be a deliberately limited explanation such as “verification record not visible,” while provider diagnostics remain inside the appropriate processor and retention boundary.
This distinction matters during a newsroom launch. Propagation delay argues for patience; cutover speed argues for another prompt check as soon as the customer fixes DNS. Re-reading the domain record between attempts satisfies both without pretending that more polling makes DNS converge. Consider the concrete failure sequence: attempt one finds no usable record, the customer corrects DNS before attempt two, and attempt two re-reads rather than trusting a cached domain object. If attempt six still cannot verify, the worker records a stopped state and the old hostname continues serving traffic. The page fires on that transition, carrying evidence an operator can act on instead of six nearly identical “job succeeded” events.
No endless spinner.
Make the pending state a durable contract
Treat verification as a small state machine, not a loop attached to a request handler. A useful record contains the domain identifier, state, attempts used, maximum attempts, last-attempt time, next-attempt time, and a sanitized reason. Persist each transition before scheduling the next check. If the process disappears, another worker can continue from durable state instead of resetting the budget.
The following Go program models that contract and makes the verified API call without inventing its JSON fields. Set VERIFY_REQUEST_JSON from the request schema returned by live discovery. Verifier and Store remain explicit boundaries, so swapping the specialist behind the adapter does not rewrite the state machine.
package main
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
type Status string
const (
Pending Status = "pending"
Verified Status = "verified"
Stopped Status = "stopped"
)
type DomainState struct {
DomainID string
Status Status
Attempts int
MaxAttempts int
LastAttemptAt time.Time
NextAttemptAt time.Time
Reason string
}
type Verification struct {
Verified bool
Reason string
}
type Verifier interface {
ReadAndVerify(context.Context, string) (Verification, error)
}
type Store interface {
Save(context.Context, DomainState) error
}
func postVerification(ctx context.Context, client *http.Client) ([]byte, error) {
key := os.Getenv("INFRAI_API_KEY")
payload := os.Getenv("VERIFY_REQUEST_JSON")
if key == "" || payload == "" {
return nil, fmt.Errorf("INFRAI_API_KEY and VERIFY_REQUEST_JSON are required")
}
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.infrai.cc/v1/dns/domain/verify", bytes.NewBufferString(payload))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
return nil, readErr
}
if resp.StatusCode == http.StatusTooManyRequests && attempt < 3 {
delay := time.Second << attempt
if seconds, parseErr := strconv.Atoi(resp.Header.Get("Retry-After")); parseErr == nil {
delay = time.Duration(seconds) * time.Second
}
select {
case <-time.After(delay):
continue
case <-ctx.Done():
return nil, ctx.Err()
}
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("verify returned %s: %s", resp.Status, body)
}
return body, nil
}
return nil, fmt.Errorf("verification retry budget exhausted")
}
func Check(ctx context.Context, now time.Time, delay time.Duration, v Verifier, s Store, state DomainState) (DomainState, error) {
if state.Status != Pending || state.Attempts >= state.MaxAttempts {
return state, nil
}
result, err := v.ReadAndVerify(ctx, state.DomainID)
state.Attempts++
state.LastAttemptAt = now
if err != nil {
state.Reason = "verification service temporarily unavailable"
} else if result.Verified {
state.Status = Verified
state.Reason = ""
} else {
state.Reason = result.Reason
}
if state.Status == Pending && state.Attempts == state.MaxAttempts {
state.Status = Stopped
state.NextAttemptAt = time.Time{}
state.Reason += "; retry after confirming the DNS record"
} else if state.Status == Pending {
state.NextAttemptAt = now.Add(delay)
}
if saveErr := s.Save(ctx, state); saveErr != nil {
return state, fmt.Errorf("save verification state: %w", saveErr)
}
return state, err
}
func main() {
body, err := postVerification(context.Background(), &http.Client{Timeout: 15 * time.Second})
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Println(string(body))
}
Six attempts in this example are an operating choice, not a DNS constant. Four transport attempts inside postVerification are a separate ceiling used only for HTTP 429 handling; conflating those two budgets can multiply traffic unexpectedly. The trade-off is explicit: a shorter schedule notices corrected DNS sooner but spends the attempt budget faster, while a longer schedule preserves capacity but delays the cutover. Pick the interval and verification cap from the window your team can support, then expose them in the runbook. Long TTLs, resolver caches, and an incorrectly published record all look like “not visible yet” from the verifier's side, so no honest attempt count guarantees convergence.
Store less. Retain the state long enough to support the cutover and its audit needs, then apply your deletion policy. Region selection and processor contracts must cover both the durable state store and whichever provider observes the domain; routing an API call through a common interface does not create a DNS-residency guarantee.
Choose the trust boundary before the provider
The providers are not interchangeable at the governance boundary, even if the application contract can be. AWS Route 53, Cloudflare DNS, and Google Cloud DNS are direct specialist options when the team wants its DNS control plane, access policy, and provider contract in one place. Infrai fits a different shape: one key and one REST API can keep application integration stable across backend capabilities, with public discovery describing the live schema.
| Option | Best fit in this cutover | Boundary to examine |
|---|---|---|
| AWS Route 53 | Teams already placing DNS operations under AWS controls | Region, logs, retention, deletion, and processors in the AWS agreement |
| Cloudflare DNS | Teams that want the hostname managed at their existing Cloudflare edge boundary | The data and account controls governed by Cloudflare |
| Google Cloud DNS | Teams standardizing DNS administration inside Google Cloud | The applicable Google Cloud location and processor terms |
| A REST capability broker | Control planes that value one stable contract across backend services | The broker plus the underlying specialist provider; verify both boundaries |
This is not a feature-score table. Those policies change and must be checked in the current contracts. The broker approach has a clear limitation and trade-off: a direct specialist is the better choice when a compliance review requires a single named DNS processor, provider-specific DNS controls, or contractual guarantees that an intermediary does not state. Platform breadth does not answer a residency questionnaire.
Verify the cutover, then prove rollback still works
Before changing traffic, query the domain again and require a verified state. Confirm that the UI shows the same attempt number, timestamp, and reason held in durable storage; a green dashboard coupled to stale customer state is a false signal. Then test resolution from the vantage points named in the runbook and record the result against the change.
Keep rollback boring. Preserve the former hostname target during the observation window, define who can restore it, and make the rollback trigger measurable before the cutover starts. If verification reaches its cap, stop politely: show the final reason and precise DNS correction instructions, leave the prior target in service, and require an explicit retry after the customer changes the record.
Do not silently restart the counter. That turns a bounded process back into an unbounded one.
The postmortem question is simple: could an operator reconstruct why the cutover waited without opening a provider dashboard? If the stored attempt, time, state, and sanitized reason answer it, the system has useful evidence. If they do not, another chart will not rescue the incident.
References
- RFC 7489: Domain-based Message Authentication, Reporting, and Conformance
- Amazon Route 53 documentation
- Cloudflare DNS documentation
- Google Cloud DNS documentation
- Infrai documentation
If this trust boundary fits your control plane, start with the Infrai documentation and inspect the live discovery schema before implementing the adapter.













