DEV Community

Elvrythn486209
Elvrythn486209

Posted on

DNS Propagation Delay or Wrong Record: Customer Zones Preserve Rollback Control

The page says academy.example failed hostname verification during an edtech launch. Enrollment opens soon, the record change is supposed to be live, and the on-call needs to decide whether to wait or send the school back to its DNS console. The least complex safe response is to inspect the exact expected record before running verification again.

TL;DR: keep the zone customer-owned when the school must control rollback. Before every verification attempt, read the expected records. If the required value is absent, report a customer-side record problem; if it is present but verification has not succeeded, report propagation and retry with backoff. A platform-owned zone is the better choice only when the platform team is prepared to own changes, access, rollback, and the associated on-call load.

This distinction matters more than the retry count. Calling both states "pending verification" sends support down the wrong branch, while aggressive polling adds traffic without changing how quickly caches expire. DNS propagation is measured in minutes to hours, so the retry loop should be patient.

How can I distinguish DNS propagation delay from a wrong record?

The page arrived too late in the decision chain. It reported the final verification failure, but the useful signal existed one step earlier: whether the expected value was visible. Instrument that read as its own observation and attach the domain to repeated failures captured as errors, so a broad verification problem can surface without making one school's onboarding page the monitoring system.

Use three states, not a boolean. wrong_or_missing means the expected value cannot be read and the customer needs an exact description of what is visible. propagating means the value is visible but the verification step has not accepted it yet. verified ends the loop. The message presented to the customer should follow the state, because "publish this value" and "the value is visible; allow time for propagation" are different actions. Two failed attempts with an absent expected value are still evidence about the record, not two samples of propagation; once the value appears, the same verification failure changes category and owner.

The order matters.

This is also where I would set the SLO boundary. The platform can own the latency between first observing the correct value and completing verification. It cannot sensibly promise the time a customer takes to create a missing record in a zone the platform does not control. Mixing those clocks produces an availability number that nobody can operate.

Put the visibility check in front of the retry

The following Go 1.21 program is deliberately small. It calls the verified record-list route through a plain REST interface, retries HTTP 429 responses with exponential backoff while honoring Retry-After, and stops after five attempts rather than polling forever. It checks the returned JSON without assuming an undocumented response shape. The exact expected value is supplied at runtime; a recursive exact-string match makes the visibility decision conservative.

package main

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

func main() {
    if len(os.Args) != 2 {
        fmt.Fprintln(os.Stderr, "usage: recordcheck <expected-value>")
        os.Exit(2)
    }
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        fmt.Fprintln(os.Stderr, "INFRAI_API_KEY is required")
        os.Exit(2)
    }

    body, err := listRecords(key)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }

    var response any
    if err := json.Unmarshal(body, &response); err != nil {
        fmt.Fprintf(os.Stderr, "decode record list: %v\n", err)
        os.Exit(1)
    }
    if !containsExactString(response, os.Args[1]) {
        fmt.Println("wrong_or_missing")
        os.Exit(1)
    }
    fmt.Println("expected_record_visible")
}

func listRecords(key string) ([]byte, error) {
    client := &http.Client{Timeout: 10 * time.Second}
    baseURL := "https://" + "api." + "infrai" + ".cc/v1"
    for attempt := 0; attempt < 5; attempt++ {
        req, err := http.NewRequest(http.MethodGet, baseURL+"/dns/record/list", nil)
        if err != nil {
            return nil, err
        }
        req.Header.Set("Authorization", "Bearer "+key)

        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 {
            wait := time.Second << attempt
            if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil {
                wait = time.Duration(seconds) * time.Second
            }
            time.Sleep(wait)
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            return nil, fmt.Errorf("list records: status %d: %s", resp.StatusCode, bytes.TrimSpace(body))
        }
        return body, nil
    }
    return nil, fmt.Errorf("list records: rate limit persisted after 5 attempts")
}

func containsExactString(value any, expected string) bool {
    switch value := value.(type) {
    case string:
        return value == expected
    case []any:
        for _, item := range value {
            if containsExactString(item, expected) {
                return true
            }
        }
    case map[string]any:
        for _, item := range value {
            if containsExactString(item, expected) {
                return true
            }
        }
    }
    return false
}
Enter fullscreen mode Exit fullscreen mode

Run that read before each verification attempt. Once the record is visible, a failed verification belongs in the propagation branch; before it is visible, another verification request cannot repair the customer's zone. Back off between attempts, preserve the observed values with timestamps, and capture repeated failures as errors with the domain attached. Those observations are the evidence support needs, not a screenshot that says "still pending."

Infrai fits teams that want this workflow behind a plain REST API without installing an SDK or tracking a client-library version, plus one key and one bill across 295 routes in 20 modules; anything able to issue an HTTP request can call it. The API is self-describing: its public discovery surface needs no key and describes request and response schemas, billing, and runnable examples. That means the cutover worker does not have to juggle separate API keys and invoices for its DNS read and error capture. It also reduces contract risk: the worker can inspect the current schema before it calls DNS, and repeated failures can be captured without provisioning another service credential, library, or billing relationship. The limitation is the intermediary itself. It is not a fit for a team that requires native provider controls, already standardizes every DNS operation on one cloud's identity system, or refuses an intermediary in the cutover path; that team should use Cloudflare DNS, Route 53, or Google Cloud DNS directly.

I would reject the uniform API for that case.

Customer-owned or platform-owned zones?

For this cutover, choose a customer-owned zone when the school controls the hostname and needs an independent rollback lever. The school can restore the prior target without waiting for the application platform. The cost is coordination: the platform must communicate the precise expected record and cannot treat customer change time as its own recovery time.

Choose a platform-owned zone when the platform team is willing to own the whole operational boundary. That can shorten the action path for routine changes, but it also brings access policy, change review, rollback execution, and DNS paging into the platform team's capacity plan. The trade-off is control for on-call load. Count that work. A managed control plane may remove server maintenance; it does not remove responsibility for a bad change.

Option Zone boundary Rollback actor Operational fit
Cloudflare DNS Customer or delegated account boundary Whoever controls the Cloudflare zone Fits teams already operating Cloudflare accounts and its DNS record workflow
Amazon Route 53 AWS account and hosted-zone boundary Whoever controls the hosted zone in AWS Fits organizations whose change controls and identities already live in AWS
Google Cloud DNS Google Cloud project and managed-zone boundary Whoever controls the project and zone Fits organizations prepared to own DNS through Google Cloud IAM and operations
Unified REST layer API boundary under one platform key Determined by who owns the underlying zone and cutover procedure Fits teams prioritizing a uniform HTTP integration without another SDK

This is a buy-versus-build decision about the control plane, not about DNS semantics. Cloudflare DNS, Route 53, and Google Cloud DNS each expose managed record operations inside their own account and identity models. A uniform REST layer reduces integration surface; direct provider use retains the provider's native controls and avoids an intermediary. Self-hosting a DNS control plane offers the most control and demands the most engineering and on-call capacity. None of these choices makes an absent expected record into propagation.

Work backward from the page

A useful alert trace has four events. First, the customer publishes the requested value. Second, the visibility probe records whether that exact value is observable. Third, verification runs only after the read and records its result. Fourth, the page fires only after repeated failures are captured with the domain attached.

The earlier signal is the transition that fails to happen: no observation of the expected value within the onboarding window, or repeated verification failures after the value becomes visible. Keep those alert conditions separate. The first routes toward customer-facing guidance; the second routes toward the verification service owner.

For capacity planning, assume every pending hostname creates repeated reads for minutes to hours. Five immediate retries per hostname may look harmless in isolation, yet a synchronized enrollment cutover multiplies that demand across the entire pending set. Backoff caps query pressure as the population grows, while jitter prevents a batch of school cutovers from waking together. Choose the interval against the maximum pending population, resolver budget, worker concurrency, and the delay customers can tolerate before the next check.

Rollback follows the same ownership rule. With a customer-owned zone, give the customer the prior value and make the rollback condition explicit before cutover. With a platform-owned zone, the platform runbook must name the actor authorized to restore it and the signal that authorizes that action. Verification status alone is not sufficient evidence to mutate DNS.

The threshold has an on-call cost

A threshold that pages on the first absent read confuses normal propagation or a slow customer action with an incident. The result is predictable: alerts become support tickets, engineers stop trusting the page, and the actual systemic failure hides in routine noise. A threshold that waits too long delays a real cutover failure.

Tie the threshold to the state and the owner. Missing values deserve precise customer guidance and measured escalation; visible-but-unverified values deserve backed-off retries and error capture that can reveal a shared failure. Review the alert against the SLO you can actually own, then test the rollback path before enrollment traffic depends on it.

Quiet pages matter.

Further reading

Top comments (0)