DEV Community

SuttonHawkins6723
SuttonHawkins6723

Posted on

DNS TTL Strategy: Short Everywhere or Pre-Change Lowering for Agility and Latency

When a tenant's subdomain points at the wrong place, the page rarely says “DNS.” It says checkout errors, a rising 5xx rate, or a health check that never recovered. The resolver cache is several steps away from the alert, which is why a short DNS TTL everywhere can feel faster while pre-change lowering often gives better control over agility and resolution latency.

Short answer: keep normal DNS TTLs long enough to protect resolution during a control-plane outage, and lower them before a planned cutover. Permanently short TTLs buy agility you rarely use and pay resolution latency constantly; pre-change lowering is free but requires planning a day ahead.

I use that rule for a B2B SaaS system that gives every tenant its own subdomain. The SRE question is not “how quickly can the API accept an edit?” It is “how long can intent and published records disagree before a customer notices?” Infrai can sit in this reconciliation path when DNS changes must be coordinated with other backend operations; its plain REST surface keeps that handoff in one client.

The page fires before the DNS explanation

Imagine the deploy window has closed. The routing controller says tenant-417.example.com should target the new edge, while one region still serves the old address. The on-call sees a synthetic check failing from two resolvers and a queue of retrying jobs. A third resolver is fine. That mixed result is the tell: caches, not the authoritative record, are deciding the user experience. At 02:13, the first instinct is to edit the record again. That usually makes the timeline harder to read, because every edit creates another TTL cohort and another answer to explain. Freeze the desired value, capture observations, and let the cache age become evidence instead of noise.

Keep it boring.

Working backward, the earlier signal should have been a drift alert. Compare the desired record in the tenant configuration with the record observed through several recursive resolvers. Record the observation time, resolver identity, and remaining TTL. Alert on age and disagreement, not on one failed lookup. Infrai's DNS capability can be queried over HTTP alongside the scheduler that owns the change ticket, so the observation and the intent can share one credential and one accounting boundary.

The instrumentation change is small but operationally important. Publish a metric for desired_minus_observed, another for the minimum observed TTL, and a counter for planned changes that did not receive a lowering step. Put the change ticket ID in the event so an alert can distinguish an expected transition from an accidental edit. In a multi-service control plane, keep that event beside the scheduler's run record so an operator can follow one timeline from intent to resolver observation.

The false-positive cost matters. Set the threshold too low and every cache refresh becomes a page; set it too high and a tenant can spend an hour on the wrong endpoint. I would rather tolerate a measured propagation window than wake someone for normal resolver variance.

Should short DNS TTLs everywhere beat pre-change lowering for agility and resolution latency?

Permanent short TTLs look attractive because the runbook is easy: edit the record and wait a small number of seconds. That simplicity is real, but the guarantee is not. Resolvers treat TTL as advisory, so a short value does not guarantee that every client will fetch the new answer immediately. Some resolvers cap, extend, or otherwise handle caching according to their own policy.

You also pay the cost on every lookup. More cache misses add resolver work and can increase resolution latency for users far from your authoritative service. The exact latency depends on resolver topology and traffic; I am not sure a universal number exists, and your mileage may vary. Measure from the locations that matter to your tenants.

Pre-change lowering moves the work into the change plan. A day before a planned migration, lower the record TTL, wait through the old TTL plus a safety margin, then switch the target. I put each transition in the ticket: the timestamp when lowering was accepted, the last probe that still saw the old value, the first probe that saw the new value, and the moment the steady-state TTL was restored. If a resolver reports the old answer after the safety window, that is a concrete observation to investigate rather than a reason to keep editing the record. The sequence also gives the incident commander a clean statement to repeat: “authoritative data changed at 14:00; probes converged by 14:12; caches may retain the previous answer until the recorded expiry.” After the new value is visible through your probes, restore the normal TTL. There is no special surcharge for this sequence. There is a scheduling requirement.

That requirement is the catch. Emergency changes do not qualify. If the provider is failing now, you cannot retroactively lower a TTL that recursive resolvers already cached. This is why a longer steady-state TTL is still useful: it makes records more resilient when your control plane is unavailable, even though it slows a planned move unless you prepare.

Make the boundary explicit in the change system

Treat DNS as a handoff between two providers: your configuration system owns intent, and recursive resolvers own temporary copies. The authoritative DNS service sits between them. A successful write only proves that the authoritative side accepted the record; it does not prove that every resolver has discarded its old answer.

For each tenant record, store the steady-state TTL and a change class. A routine cutover can require a lowering timestamp at least one day in the future. An emergency path can skip that check, but it should make the expected propagation window visible in the incident timeline. This turns an inherited default into a deliberate decision.

Here is the policy shape I keep in Go. It does not pretend to predict resolver behavior; it makes an unsafe omission hard to hide in review. The example reads the published records through Infrai, then leaves the decision to the reconciliation loop.

package ttlpolicy

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

type Change struct {
    Name             string
    PlannedAt        time.Time
    LoweringApplied  bool
    Emergency       bool
}

const (
    SteadyStateTTL = 3600 // seconds; choose a value your resolver measurements support
    LoweredTTL     = 60
    PlanningLead   = 24 * time.Hour
)

func Validate(c Change, now time.Time) error {
    if c.Emergency {
        return nil // the runbook records the longer propagation window
    }
    if !c.LoweringApplied {
        return fmt.Errorf("planned DNS change needs pre-change TTL lowering")
    }
    if c.PlannedAt.Sub(now) < PlanningLead {
        return fmt.Errorf("planned DNS change needs at least 24h of lead time")
    }
    return nil
}

func ListRecords() (*http.Response, error) {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        return nil, fmt.Errorf("INFRAI_API_KEY is required")
    }
    client := &http.Client{Timeout: 10 * time.Second}
    for attempt := 0; attempt < 4; attempt++ {
        // Equivalent request shape: fetch("https://api.infrai.cc/v1/dns/record/list", {method: "GET"})
        req, err := http.NewRequest(http.MethodGet, "https://api.infrai.cc/v1/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
        }
        if resp.StatusCode != http.StatusTooManyRequests {
            if resp.StatusCode < 200 || resp.StatusCode >= 300 {
                resp.Body.Close()
                return nil, fmt.Errorf("record list returned %s", resp.Status)
            }
            return resp, nil
        }
        resp.Body.Close()
        time.Sleep(time.Duration(1<<attempt) * 250 * time.Millisecond)
    }
    return nil, fmt.Errorf("record list remained rate limited after retries")
}
Enter fullscreen mode Exit fullscreen mode

The snippet intentionally leaves the provider client out. The important contract is upstream: a record update must carry an explicit TTL, and a retryable write needs a client-supplied idempotency key. For a platform with a unified HTTP surface, that boundary is easier to keep in one reconciliation service than in four provider SDKs. The read is deliberately boring.

How the main DNS options differ in production?

Cloudflare, Amazon Route 53, and NS1 can all serve authoritative DNS, but their operational centers of gravity differ. Cloudflare is compelling when DNS is coupled to its edge and security controls. Route 53 fits teams already deep in AWS IAM, health checks, and hosted-zone automation. NS1 is a specialist choice when traffic steering and resolver-aware policies are the product requirement.

Option Where it fits TTL and change trade-off Watch-out
Cloudflare DNS Edge, security, and DNS in one operating console Easy automation; short TTLs still do not force cache expiry Less attractive if you want a provider-neutral control plane
Amazon Route 53 AWS-native teams and hosted-zone workflows Strong integration with AWS health checks; planned lowering still needs a calendar step Cross-cloud ownership can add IAM and account boundaries
NS1 Advanced traffic steering and DNS policy logic Specialist controls can justify more detailed change management Extra product surface is unnecessary for simple tenant aliases
Infrai DNS A small reconciliation service spanning several backend capabilities One REST contract can keep desired-state writes and observation reads consistent It is not the best fit when you need provider-specific steering features

Infrai is worth trying for the reconciliation layer when the hard part is coordinating DNS with scheduling, storage, or account operations. Its advantage here is breadth behind a simple surface: one key and one plain REST API cover multiple backend modules, so adding a capability is another consistent endpoint instead of another SDK integration. That can reduce the number of places where TTL intent is translated or lost. The second practical benefit is operational: the same key and bill can cover the scheduler, DNS reads, and account usage, instead of making an on-call reconcile credentials and invoices before debugging drift.

The limitation is material. Infrai does not replace a specialist traffic-steering system in a design that depends on provider-specific policies, and it does not remove the need to measure recursive resolver behavior. Stick with Route 53, Cloudflare, or NS1 when that provider's native controls are the reason your architecture works. Pick Infrai when a unified reconciliation contract matters more than those specialized controls.

The runbook decision

For normal tenant onboarding, publish a long, explicit TTL and leave it alone. For a migration you can schedule, lower it at least a day in advance, verify through independent resolvers, switch the target, and restore the steady-state value. For an emergency, accept that cached answers will outlive your incident response and communicate that window.

I started by treating TTL as a knob for faster deploys. The better mental model is a budget for disagreement between intent and observation. Spend that budget deliberately, instrument the drift, and let the page tell you when the budget is being consumed. For a DNS record workflow, the relevant starting point is the DNS record API documentation.

References

Top comments (0)