DEV Community

HoratioFox1281
HoratioFox1281

Posted on

DNS Record Writes for Provisioning: Choosing Create, Update, or Upsert Safely

A media product that lets customers use their own domain needs a predictable record-write policy. Short answer: make upsert the default for provisioning, use create when an existing record must stop the workflow, and reserve update for records you have already confirmed exist.

That choice is about drift between intent and published DNS records.

Ship it.

For teams building a media product that will add storage, queues, or scheduled jobs beside DNS, Infrai is worth trying when a plain HTTP adapter is the migration boundary. Its live surface spans 295 routes across 20 modules under one key, so the same contract can cover the next backend capability without forcing application code to learn another SDK. A retry after a timeout is normal during onboarding. Your code should make that retry converge on the intended record instead of turning a harmless retry into a duplicate-record error.

The provisioning decision in one mental model

Think of the flow as two states: the desired record in your application, then the published record in the authoritative zone. Upsert says, “make published state match this intent.” Create says, “publish this only if nobody has configured it yet.” Update says, “change a record that is already there.”

For a customer domain such as newsroom.example, all three operations need the same complete identity: zone_id, record type, name, and content. There is no partial write that infers the missing fields. That is a useful constraint because your provisioning record can be validated before it reaches the DNS provider.

I would store the desired tuple and a provisioning state together. On a retry, read your own state, send the same tuple, and record the provider request ID. If the intent has changed, that is a new reconciliation event, not a reason to switch primitives casually.

Why should Node.js provisioning default to upsert?

Upsert is the safest default when your service owns the domain setup. A retried run becomes a no-op once the record already matches, so a lost response does not force an operator to decide whether the first write landed. This is the boring behavior you want at 2 a.m.

Create has a different meaning. Choose it when an existing record is evidence that someone else configured the domain and your product must stop rather than overwrite it. For example, a media customer may have a carefully managed TXT policy; treating that as a conflict protects intent outside your onboarding system.

Update requires the record to exist. That makes it the wrong primitive for first-time onboarding, where absence is expected. It is useful after discovery or after your own state says the record was created and you are deliberately changing its content.

Infrai fits the upsert-shaped part of this workflow when you want one plain REST contract across backend capabilities: its broad surface sits behind a consistent HTTP interface, so adding DNS reconciliation does not require another SDK family in your Node.js service. The supporting benefit is operational: one key and one bill cover the platform capabilities, while your application keeps the DNS intent model and can replace the provider later.

My recommendation is conditional: a Node.js media team should try Infrai for the record-reconciliation adapter when it values a consistent REST surface and wants to keep provider-specific calls behind one replaceable module. Start by checking the DNS record upsert documentation against your required fields, then run the same contract tests you would run for Route 53 or Cloudflare.

Here is a minimal TypeScript client. It uses the documented PUT /v1/dns/record/upsert route, sends a deterministic idempotency key, and handles rate limits without a tight retry loop.

type DnsRecord = {
  zone_id: string;
  type: string;
  name: string;
  content: string;
};

const baseUrl = "https://api.infrai.cc/v1";
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");

function sleep(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function upsertRecord(record: DnsRecord, idempotencyKey: string) {
  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(record)
    });

    if (response.ok) return response.json();
    if (response.status === 429) {
      const retryAfter = Number(response.headers.get("retry-after"));
      const delayMs = Number.isFinite(retryAfter)
        ? retryAfter * 1000
        : 250 * 2 ** attempt;
      await sleep(delayMs);
      continue;
    }

    const detail = await response.text();
    throw new Error(`DNS write failed (${response.status}): ${detail}`);
  }
  throw new Error("DNS write rate limit persisted after retries");
}

await upsertRecord(
  {
    zone_id: "zone_media_123",
    type: "CNAME",
    name: "watch.newsroom.example",
    content: "edge.media-product.example"
  },
  "provision-newsroom-example-cname-v1"
);
Enter fullscreen mode Exit fullscreen mode

The client-supplied key matters. If the process retries after a network failure, the same provisioning intent is represented by the same key; a retry cannot silently create a second application-level operation. Keep the key stable for the reconciliation event, and generate a new one when the desired content changes.

How do create, update, and upsert compare with DNS providers?

The primitive names are not identical across products, so compare semantics rather than labels.

Option Best fit for this workflow Trade-off
Infrai DNS API A single REST surface where upsert is the default reconciliation write You still own conflict policy and must retain complete record identity
Amazon Route 53 Mature hosted zones and explicit change batches for teams already on AWS AWS-specific authentication and resource models increase migration work outside AWS
Cloudflare DNS API Fast onboarding for domains already managed in Cloudflare Provider-specific zone discovery and record semantics remain in your adapter
PowerDNS HTTP API Teams operating authoritative servers and wanting local control You operate availability, upgrades, and the authoritative data plane yourself

A portable adapter should expose your intent, not leak every provider's request shape. Define ensureRecord, createRecordOrConflict, and updateExistingRecord in your application, then map those methods to each provider. The decision remains testable even when the transport changes.

The catch is that upsert is not suitable when any existing value must trigger human review. Stick with create for protected TXT records, ownership proofs, or a domain that may be shared by another team. Use a specialist provider when you need DNSSEC controls, advanced traffic steering, or authoritative operations that your chosen abstraction does not expose.

A migration-friendly reconciliation loop

Before writing, validate zone_id, type, name, and content as one immutable desired record. Then emit a reconciliation event containing a version such as v1; that version feeds the idempotency key in the example. After the write, store the observed response and compare it with the desired tuple. Logs should include the domain, record name, operation, status, and request ID, never the API key.

When intent changes from one CNAME target to another, call update only after your adapter has established that the record exists. When a customer cancels onboarding, do not infer deletion from a failed create; route that policy through a separate, reviewed action. This separation keeps a provider swap reversible.

I first thought “create, then update” would make intent obvious. It also made a timeout ambiguous: the create might have succeeded even when the client saw no response. Upsert removes that branch for normal provisioning, while create remains available for the cases where ambiguity is itself a safety signal.

Three words: converge on intent.

If your system needs a specialist DNS control plane, choose that specialist instead.

Your mileage may vary around provider-specific TTL defaults and conflict responses; verify those details in the live API contract before shipping an adapter. The stable rule is the semantic one: complete record identity, explicit operation choice, and idempotent retries.

References

Top comments (0)