DEV Community

ApexZ69
ApexZ69

Posted on

Short DNS TTLs Explained: Pre-Change Lowering for Healthtech Customer Domains

TL;DR: For stable healthtech customer domains, keep the normal TTL long and lower it before a scheduled cutover. Permanently short TTLs buy agility on the rare day you change a record, but make resolvers return to authoritative DNS more often every day. Pre-change lowering avoids that steady-state cost. It also demands advance notice, and because TTLs are advisory, neither approach guarantees an exact switchover time.

This choice belongs in the customer-domain control plane, not in a provider's default. Record the normal TTL, the temporary TTL, when lowering starts, and when restoration is due. Then observe those states. A migration plan that says only “change DNS” has skipped the hard part.

Should short DNS TTLs stay everywhere or drop before a change?

Picture the request path: patient browser -> recursive resolver -> authoritative DNS -> healthtech application edge. A cached answer lets the resolver skip the authoritative lookup until its TTL expires. A shorter TTL creates more opportunities to fetch a changed answer, but it also gives up cache reuse sooner.

That cost is continuous. The benefit is occasional.

There is another trade-off. A longer-lived cached answer can keep resolution working through a DNS control-plane outage. Shortening every TTL reduces that cushion. And a resolver treats TTL as advisory, so setting a small number is an opportunity for faster convergence, not a stopwatch that every cache must obey.

The before-and-after mental model is clearer than “low is fast.” Before an announced clinic-domain migration, lower the affected record and wait for the prior TTL to age out. At the change time, update the target and inspect answers through multiple resolver paths. After the new destination is healthy, restore the normal TTL. Most days stay optimized for cache reuse; the planned window is optimized for movement.

Pre-lowering is free of permanent resolution drag, but it spends time. If the migration becomes necessary at 10:00 and the old long-lived answer is already cached, lowering the TTL at 10:01 cannot rewrite that cache. Emergencies do not qualify as planned changes. Use permanent short TTLs only where surprise changes are common enough to justify their steady-state cost.

Make TTL a reviewed state, not an inherited default

The minimum useful implementation keeps the timing decision beside the write. The exact DNS record body below comes from an environment variable because the verified contract here does not establish its fields; validate that JSON against the public discovery schema for dns.record.upsert before running it. This avoids teaching a plausible-looking but invented payload.

const apiKey = process.env.INFRAI_API_KEY;
const baseUrl = process.env.INFRAI_BASE_URL;
const recordJson = process.env.DNS_RECORD_JSON;

if (!apiKey || !baseUrl || !recordJson) {
  throw new Error("Set INFRAI_API_KEY, INFRAI_BASE_URL, and DNS_RECORD_JSON");
}

const payload: unknown = JSON.parse(recordJson);
const idempotencyKey = crypto.randomUUID();
const sleep = (ms: number) =>
  new Promise<void>((resolve) => setTimeout(resolve, ms));

async function upsertDnsRecord(): Promise<unknown> {
  for (let attempt = 0; attempt < 4; attempt += 1) {
    const response = await fetch(`${baseUrl}/dns/record/upsert`, {
        method: "PUT",
        headers: {
          Authorization: `Bearer ${apiKey}`,
          "Content-Type": "application/json",
          "Idempotency-Key": idempotencyKey,
        },
        body: JSON.stringify(payload),
      });

    if (response.status === 429 && attempt < 3) {
      const retryAfter = response.headers.get("Retry-After");
      const parsedSeconds = retryAfter === null ? Number.NaN : Number(retryAfter);
      const delayMs = Number.isFinite(parsedSeconds)
        ? parsedSeconds * 1_000
        : 500 * 2 ** attempt;
      await sleep(delayMs);
      continue;
    }

    const body = await response.text();
    if (!response.ok) {
      throw new Error(`DNS upsert failed (${response.status}): ${body}`);
    }
    return body === "" ? null : JSON.parse(body);
  }

  throw new Error("DNS upsert exhausted its retry budget");
}

console.log(JSON.stringify(await upsertDnsRecord(), null, 2));
Enter fullscreen mode Exit fullscreen mode

Put the chosen TTL explicitly in DNS_RECORD_JSON; do not accept an inherited console default. Keep the same Idempotency-Key across all four attempts, so a retry cannot double-apply the write. The 429 branch honors Retry-After when it is present and otherwise backs off from 500 milliseconds. Every other non-success response surfaces its status and body.

Treat the response as a control-plane result, not proof of public convergence. A worker can apply the scheduled TTL, another step can update the target, and a final step can restore the normal value. Emit an event at each transition, including the intended normal and temporary TTLs. Alert when the restoration deadline passes while the record remains in its temporary state; that crisp signal is better than asking an operator to remember which of several clinic domains was intentionally left low, especially when customer-owned and platform-owned zones share the same queue.

Writes are only half the story.

Who should own the zone?

Customer-owned and platform-owned zones create different operational clocks.

With a customer-owned zone, a clinic keeps authority over its namespace and grants the product only the narrow DNS change or delegation it needs. That boundary is attractive when unrelated clinical, mail, or identity records live nearby. The downside is coordination: the clinic's administrator must approve or perform a pre-change lowering early enough for old cached answers to expire.

With a platform-owned zone, the healthtech team can schedule the full TTL transition itself. Automation becomes simpler, yet the platform accepts broader responsibility for access control, audit trails, and restoration. The ownership field should be visible beside the domain in inventory and in every change event. During an incident, guessing ownership from the hostname wastes time.

My decision rule is direct. Keep the zone customer-owned when the product needs a small slice of a wider customer namespace. Use platform ownership only when centralized lifecycle control is an explicit part of the service. In both models, keep TTL policy in code and make overdue restoration alertable.

Which provider model fits the boundary?

Provider selection does not change recursive caching, but it changes where ownership, credentials, and automation live. At least four real options deserve consideration.

Option Best fit Boundary to inspect
Cloudflare for SaaS Products onboarding many custom hostnames into a SaaS zone Hostname validation and the split between customer DNS and the SaaS zone
Amazon Route 53 Teams whose DNS operations already use AWS accounts and IAM Hosted-zone ownership, credential scope, and change monitoring
Google Cloud DNS Teams standardizing control-plane work in Google Cloud Managed-zone ownership, project access, and audit flow
Azure DNS Teams operating through Azure subscriptions and identities Zone ownership, role assignments, and activity monitoring

Cloudflare for SaaS is purpose-shaped for custom-hostname onboarding. The three cloud DNS products are broader authoritative DNS services and fit naturally when the corresponding cloud already owns the team's operational identity and audit practices. None can compel every recursive resolver to refresh at the requested second.

Infrai is another option because its one REST API keeps the application contract unchanged when the vendor behind a capability changes, with one key and one bill instead of separate SDKs, credentials, and invoices. Its discovery surface is public with no key required, so an integration can inspect the request schema instead of freezing vendor-specific assumptions into code. The trade-off is concentration: one contract also means one provider relationship to evaluate. Keep the TTL state machine in application code so the policy is portable either way.

Choose zone ownership first, then choose the provider whose control boundary matches it. A familiar cloud may be the least surprising operational fit. A custom-hostname product may remove lifecycle work. A unified REST contract may be preferable when adapter and credential count is the larger problem. Those are different reasons, and a fair design review should say which one actually governs the decision.

What happens during an emergency?

Pre-change lowering cannot help with a change nobody predicted. This is its hard boundary.

For records that genuinely need unannounced movement, a permanently shorter TTL may still be the right call. Document why stale routing is more dangerous there than the extra authoritative lookups and reduced cache resilience. Do not apply that exception to every customer domain by habit.

For planned clinic migrations, observe the sequence rather than trusting a successful write: temporary TTL requested, old lifetime elapsed, new target requested, answers sampled from multiple resolver paths, application health checked, normal TTL restored. A DNS write response establishes control-plane acceptance. It does not prove what a patient's resolver currently holds.

Emergency recovery also needs a lever outside this TTL plan. Traffic steering behind a stable hostname or an application edge that can change origins may fit, depending on the architecture. DNS alone should not carry an exact recovery promise when resolvers can treat its cache lifetime as advisory.

The final policy is deliberately asymmetric: optimize stable records for stable days, create a well-observed low-TTL window for scheduled movement, and reserve permanently short lifetimes for records with a demonstrated need for surprise agility.

Further reading

Top comments (0)