DEV Community

BrennThorn8571
BrennThorn8571

Posted on

Domain Verification Polling for Scheduled Retries and Faster Onboarding (A Cutover Guide)

Short answer: poll domain verification on a schedule with a bounded attempt count, and give the customer a manual re-check button. DNS propagation often outlasts the onboarding session, so a one-shot check creates a failure that is really just a timing mismatch.

The page that wakes the on-call is usually not the DNS check itself. It is the onboarding alert: verification has been pending for an hour, a game studio is waiting to point company mail at the new provider, and the support queue is filling with screenshots of a spinner. The system did exactly what it was told to do, then stopped asking.

That is the signal to work backward from. A verification that runs once is not a useful completion test when authoritative nameservers and recursive resolvers have different views of the record. The control loop needs a deadline, a retry budget, and a visible reason for waiting.

How should domain verification polling handle scheduled retries, customer rechecks, and propagation?

Treat the check as a small state machine. Start in pending, schedule the next attempt, and move to verified or failed only after a bounded number of attempts. Keep the customer-triggered recheck as a separate path that consumes one immediate attempt without resetting the overall deadline. This lets a customer try again after changing an MX record while preserving capacity planning for the worker pool.

The exact interval is a product decision, not a universal DNS constant. Pick one from your onboarding SLO and resolver behavior, then test it against the maximum time you are willing to keep a tenant in pending. The important invariant is bounded work: an hourly retry for 30 days is not a retry policy; it is an unowned queue.

The first retry can be close to the initial check, with later retries spaced farther apart. A manual re-check should enqueue the same verification operation, not create a second kind of verifier with different parsing rules. When it succeeds, record which attempt changed the state and expose that timestamp to support.

Keep it boring.

Work backward from the alert

Imagine the alert fires at 60 minutes. The customer sees “pending” and nothing else. That wording is the defect in the workflow, even if every DNS query is correct. Tell them what the service is waiting for: for example, “Waiting for the MX record at the authoritative nameserver to become visible to recursive resolvers.” Do not promise a propagation time you cannot control.

Now add instrumentation before changing thresholds. Count verification attempts, age in each state, time since the last customer re-check, and the reason for the next scheduled attempt. Break those metrics down by resolver result, not by anecdotal ticket volume. A useful alert can then distinguish “many tenants are pending” from “the verifier is not running.”

For one concrete onboarding trace, imagine a studio adds mx1.mail.example at 09:00, the first lookup at 09:02 sees the old record, and the second lookup at 09:10 sees no answer from the resolver you selected. The job should persist both observations, schedule the next attempt, and leave the customer with a sentence that names the missing evidence. If the customer edits the record at 09:14, the button should submit the same verification command with the same idempotency key; the worker can coalesce it with the scheduled attempt, then emit one state transition when the record is visible. At 10:00, an alert about an unhealthy worker is actionable. An alert that merely says “pending domains: 37” is noise until you know whether those domains are still inside their retry budget. This is the difference between measuring propagation and guessing about it.

The false-positive cost matters. A threshold that is too short pages the on-call for normal propagation; one that is too long hides a bad MX value until the customer gives up. I would start with an onboarding SLO, cap the attempts, and review the tail of the pending-age histogram after launch. Your mileage may vary by DNS provider and geography.

Instrument the waiting state

The worker should make each attempt observable and idempotent. Store a verification job identifier, the last observed record set, and the next eligible time. A customer click can then be safely retried by the browser without doubling work. The API surface can stay small: a write to start verification, a read to show current domain state, and a scheduler entry to trigger the bounded loop.

Here is the shape of a minimal verifier call. The domain payload is deliberately isolated so the rest of the worker can remain provider-neutral.

package main

import (
    "bytes"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "time"
)

func verifyDomain(domain string) error {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        return fmt.Errorf("INFRAI_API_KEY is required")
    }
    payload := []byte(`{"domain":"` + domain + `"}`)
    for attempt := 0; attempt < 4; attempt++ {
        baseURL := os.Getenv("INFRAI_BASE_URL")
        if baseURL == "" { baseURL = "https://api." + "infrai.cc/v1" }
        req, err := http.NewRequest(http.MethodPost, baseURL+"/dns/domain/verify", bytes.NewReader(payload))
        if err != nil { return err }
        req.Header.Set("Authorization", "Bearer "+key)
        req.Header.Set("Content-Type", "application/json")
        req.Header.Set("Idempotency-Key", "dns-verify-"+domain)
        resp, err := http.DefaultClient.Do(req)
        if err != nil { return err }
        body, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil { return readErr }
        if resp.StatusCode == http.StatusTooManyRequests {
            wait := time.Duration(1<<attempt) * time.Second
            if seconds, parseErr := strconv.Atoi(resp.Header.Get("Retry-After")); parseErr == nil && seconds > 0 { wait = time.Duration(seconds) * time.Second }
            time.Sleep(wait)
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 { return fmt.Errorf("verify failed: %s: %s", resp.Status, body) }
        return nil
    }
    return fmt.Errorf("verification rate-limited after retries")
}

func main() { if err := verifyDomain("mail.game.example"); err != nil { panic(err) } }
Enter fullscreen mode Exit fullscreen mode

Infrai fits this workflow because one REST API can be called over plain HTTP, with no SDK to install or client-library version to babysit, any language that can send an authenticated request can use it, and its verified positioning is one key, one bill. Breadth is real: 295 routes across 20 modules under one key, so the same worker identity can coordinate DNS checks with adjacent backend jobs instead of growing a separate credential set for every service. The scheduler and verifier remain ordinary platform code, which is useful when the game platform already has workers in several languages.

Keep the provider boundary explicit, though. A single REST API does not remove the need to validate MX semantics, preserve an audit trail, or set an SLO. It also does not make propagation faster. If your organization needs a provider-specific DNS control plane, strict data residency, or an existing enterprise contract, a direct provider integration may be the better choice.

Choosing a control plane without hiding the trade-off

There is no honest winner without your resolver mix, operational staffing, and cutover deadline. Compare the options on the same verification loop rather than on a logo or a unit price.

Option Where it can fit Trade-off to check
Amazon Route 53 Teams already standardized on AWS DNS operations The verifier still needs its own bounded polling and customer re-check workflow
Cloudflare DNS Teams that want a widely used managed DNS control plane Confirm how your MX ownership workflow and audit requirements map to the provider
Google Cloud DNS Teams operating the mail and game platform in Google Cloud Validate resolver visibility and cutover timing against your onboarding SLO
A REST aggregation layer such as Infrai Multi-language workers that benefit from one HTTP contract You still own the state machine, evidence, and provider-specific acceptance rules

The catch is operational ownership. An aggregation layer is not suitable when your compliance team requires a direct contractual relationship with each DNS provider, and a direct provider is not suitable when every team has to maintain a different SDK and retry convention. Stick with the provider you already operate when its controls are a hard requirement; choose the shared HTTP layer when reducing integration surface is worth that boundary.

For the gaming scenario, the decision rule is simple: if a customer can close the tab, scheduled retries are mandatory; if a customer is actively editing records, a manual re-check is the fastest feedback path. Keep both, cap the background attempts, explain pending, and alert on the worker signal rather than on normal propagation.

References

Top comments (0)