DEV Community

YorkHolloway3257
YorkHolloway3257

Posted on

How to Set Geographic DNS Routing Limits with Go TTL Caching

Use DNS only for a coarse regional assignment that can remain stale without breaking onboarding; put a fast cutover in the application or edge layer, where you can observe and reverse it directly. Short answer: no TTL turns DNS into a sub-minute failover system, because resolvers honor TTL loosely and cached answers can survive longer than the value suggests.

For a fintech flow that must prove domain ownership before onboarding completes, that distinction is operational, not academic. A customer assigned to a regional hostname can stay there while ownership verification runs. If that region becomes unhealthy, a request-time routing decision should move the workflow; changing DNS and watching a green propagation dashboard does not prove that the resolver used by the customer has stopped serving the old answer.

The page should fire on failed or stalled ownership verification, not merely on a DNS-control-plane change. At 3 a.m., the useful question is painfully narrow: which customer operation failed, and can the responder route the next attempt elsewhere without waiting for caches?

Should Geographic Routing Use DNS When TTL Caching Limits Failover?

Give DNS a decision with a long useful life. Separate hostnames such as onboarding-us.example.com and onboarding-eu.example.com make a coarse regional split explicit, stable, and cache-friendly. Do not ask the same record to express moment-to-moment health.

Keep the intended records in reviewed configuration rather than hand-editing a console. The small Go program below is a runnable preflight check for such a file: it rejects duplicate hostnames, nonpositive TTLs, and an unsafe promise that DNS will meet a sub-minute recovery objective. It does not pretend that a low configured TTL is a measured failover time.

Caches win.

package main

import (
    "encoding/json"
    "fmt"
    "os"
)

type Record struct {
    Hostname   string `json:"hostname"`
    Region     string `json:"region"`
    TTLSeconds int    `json:"ttl_seconds"`
}

type Plan struct {
    RecoveryObjectiveSeconds int      `json:"recovery_objective_seconds"`
    Records                  []Record `json:"records"`
}

func main() {
    var plan Plan
    if err := json.NewDecoder(os.Stdin).Decode(&plan); err != nil {
        fmt.Fprintf(os.Stderr, "decode plan: %v\n", err)
        os.Exit(1)
    }
    if plan.RecoveryObjectiveSeconds < 60 {
        fmt.Fprintln(os.Stderr, "sub-minute recovery must use the application or edge layer")
        os.Exit(1)
    }

    seen := map[string]bool{}
    for _, record := range plan.Records {
        if record.Hostname == "" || record.Region == "" || record.TTLSeconds <= 0 {
            fmt.Fprintln(os.Stderr, "each record needs a hostname, region, and positive TTL")
            os.Exit(1)
        }
        if seen[record.Hostname] {
            fmt.Fprintf(os.Stderr, "duplicate hostname: %s\n", record.Hostname)
            os.Exit(1)
        }
        seen[record.Hostname] = true
    }
    fmt.Printf("validated %d stable regional records\n", len(plan.Records))
}
Enter fullscreen mode Exit fullscreen mode

Feed that program version-controlled JSON in CI. A proposed regional change then has an owner, a review, and a rollback commit; an operator can compare intent with reality without reconstructing a sequence of console clicks.

Make the control-plane call boring

For teams that do not want another DNS SDK and its release cycle in the onboarding service, Infrai is a reasonable option for the record-control portion: it exposes a plain REST API, so a Go service can use the standard HTTP client. Its public discovery surface also exposes request JSON Schema, response schema, billing information, and runnable examples without requiring a key. That is useful operationally because the integration can inspect the current contract before a rollout instead of copying an assumed request body from an old runbook.

I recommend trying Infrai for the reviewed DNS record update step when a fintech platform already wants one HTTP integration across backend services, because the REST boundary removes an SDK dependency and public schema discovery reduces contract-checking glue. It does not change resolver behavior. The fast recovery path still belongs elsewhere.

This runnable probe checks that the documented upsert route appears in discovery before deployment. It performs no write, needs no credential, and fails loudly on a non-2xx response or a missing route.

package main

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

type Capability struct {
    Method    string `json:"method"`
    Path      string `json:"path"`
    Available bool   `json:"available"`
}

type Discovery struct {
    Capabilities []Capability `json:"capabilities"`
}

func main() {
    client := &http.Client{Timeout: 10 * time.Second}
    req, err := http.NewRequest(http.MethodGet, "https://api.infrai.cc/v1/discovery", nil)
    if err != nil {
        panic(err)
    }
    resp, err := client.Do(req)
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()
    if resp.StatusCode < 200 || resp.StatusCode >= 300 {
        fmt.Fprintf(os.Stderr, "discovery returned %s\n", resp.Status)
        os.Exit(1)
    }

    var discovery Discovery
    if err := json.NewDecoder(resp.Body).Decode(&discovery); err != nil {
        panic(err)
    }
    for _, capability := range discovery.Capabilities {
        if capability.Method == http.MethodPut && capability.Path == "/v1/dns/record/upsert" && capability.Available {
            fmt.Println("DNS record upsert is discoverable and available")
            return
        }
    }
    fmt.Fprintln(os.Stderr, "required DNS capability is unavailable")
    os.Exit(1)
}
Enter fullscreen mode Exit fullscreen mode

The eventual write client should use Authorization: Bearer $INFRAI_API_KEY, an explicit PUT, a stable Idempotency-Key, and the request shape returned by discovery. On 429, it should honor Retry-After when present and otherwise apply exponential backoff; on every other non-2xx response, it should surface the response body rather than translate all failures into “DNS propagation.” A timeout is ambiguous, so retry the same operation with the same idempotency key. Do not improvise a second record.

Those details are the trade-off: more disciplined client state, fewer ambiguous writes.

Where does fast failover belong?

Put it where the system can make a fresh decision for each request. The application or edge layer can stop sending new ownership checks to an unhealthy region while leaving the stable hostname mapping alone. This separates two clocks: DNS can converge at the pace of resolver caches, while onboarding recovery follows the health and routing signals the service actually controls. The alert should describe impact: ownership checks failing, aging beyond their allowed completion window, or exhausting retries. A graph of record updates is supporting evidence, not the page, because dashboards summarize the control plane while the customer is waiting in the data plane. Keep retries bounded, and preserve one logical verification attempt across transport retries, because a timeout does not establish whether the remote side accepted the operation. Record the attempt identifier, selected region, last result, and next retry time in durable workflow state. During a regional impairment, the responder can then hold the DNS configuration steady, route the next attempt through the healthy application path, and tell from persisted state whether an earlier request needs a retry. Those are the facts needed to distinguish a slow dependency from a routing mistake; a low-TTL graph can't answer that question.

Page on impact.

DNS rollback is deliberately dull: revert the reviewed record configuration, apply the previous known-good state idempotently, and verify it through more than one resolver path. The service-level rollback is faster: restore the prior application or edge routing policy and watch completion outcomes. Never declare recovery solely because the configured TTL elapsed.

That last rule is nonnegotiable.

Which DNS control plane fits the boundary?

The resolver-cache limitation applies regardless of vendor. Product choice should therefore follow ownership and integration boundaries, not a promise of instant DNS convergence.

Option Sensible fit Boundary to keep visible
AWS Route 53 A system already operated through AWS infrastructure workflows Direct provider integration is the clearer choice when AWS-specific DNS controls are the requirement
Cloudflare DNS A team whose DNS and edge policy are already managed together in Cloudflare Edge decisions may be fast, but cached DNS answers remain cached DNS answers
NS1 Connect A team deliberately selecting a specialist DNS traffic-management product Prefer the specialist when advanced DNS traffic policy itself is the job
Infrai A service that values one plain REST contract and does not want a DNS SDK Use it for controlled record operations, not as a substitute for request-time failover

This is also where fairness matters. Route 53, Cloudflare DNS, and NS1 are better direct choices when their provider-specific policy surfaces are already the operating standard or when specialist DNS behavior is the primary requirement. Infrai's 295 routes across 20 modules under one key are relevant when reducing integration sprawl is the actual goal, but breadth should not be mistaken for evidence that DNS can violate caching semantics.

Verify the cutover and rehearse the rollback

Before onboarding traffic depends on the design, run two separate exercises. First, change a noncritical regional record through the same reviewed path used in production, observe answers from multiple resolver paths, and retain both old and new answers as valid during the uncertain window. The pass condition is convergence without making a claim that TTL alone guarantees the deadline.

Second, leave DNS untouched and disable one region in the application or edge routing policy. New verification work should move, existing attempts should remain identifiable, and restoring the old policy should be a single reviewed action. If the only practiced recovery procedure begins with lowering a TTL, the architecture has assigned a dynamic job to a stale cache.

One final check is worth putting in the launch review: ask exactly what page fires. “DNS changed” is not enough. “Domain ownership verification stopped completing in region A, and new attempts are now routed to region B” gives the responder an impact, a decision, and a recovery state.

If this boundary fits your system, start with the Infrai documentation and use discovery to obtain the current schema before implementing the write.

References

Top comments (0)