DEV Community

NicodemusChristensen2675
NicodemusChristensen2675

Posted on

Node.js DNS Upsert or Create Across 4 Provisioning Failure Modes

Short answer: use create when an existing DNS record is evidence of a conflict that must stop an e-commerce hostname cutover. Use upsert when the controller already owns that record set and a repeated write must converge after a timeout. For a mixed cutover, create the ownership-sensitive records first, then upsert only the records covered by an explicit ownership manifest.

Decision Pick it when Failure you preserve Evidence required before proceeding
Create The name is expected to be absent Existing owner or stale configuration An authoritative lookup matches the newly created value
Upsert The controller owns the exact name and type Retry can repair an unknown write outcome Authoritative lookup matches the desired value and change ID
Read, compare, then update A record may be shared or manually managed Drift remains visible Previous value, desired value, actor, and authoritative result
Stop and inspect Ownership or mail evidence is ambiguous No destructive guess is made Operator decision recorded against the cutover

That rule favors deliverability evidence over a deceptively tidy provisioning status. A DNS API can accept a mutation while recursive resolvers still hold older data. Mail receivers can also evaluate SPF, DKIM, and DMARC independently. “Write succeeded” is therefore an event in the workflow, not the definition of success.

Should DNS provisioning use upsert or create for the failure you want?

Create and upsert make different failures observable. Create exposes a collision. If shop.example.test already has the record set your provisioner planned to add, the operation should fail rather than silently replacing somebody else's value. That failure is useful during customer onboarding, account recovery, and delegated-zone changes because it keeps uncertain ownership in view.

Upsert exposes less conflict information, but it handles an important distributed-systems ambiguity. Imagine that a worker sends a DNS mutation, the provider commits it, and the response is lost. A retry of create can report “already exists” even though the first attempt achieved the desired state. An upsert retry can converge on the same value.

Fast retries are not proof.

Consider the full failure timeline. At 10:00, a provisioning worker reads no record and submits create. The authoritative service commits the record, but the worker loses its response and records an uncertain attempt rather than success. At 10:01, a queue redelivery repeats create and receives a conflict. Treating every conflict as somebody else's ownership would strand a correct change; treating every conflict as permission to overwrite would conceal a real collision. The safe branch is narrower: read the exact record set from the authority, normalize it by record type, and compare it with the desired snapshot tied to the original change ID. An exact match lets the state machine mark the mutation verified. A different value stops automation and preserves both versions for review. Upsert avoids this particular retry branch, but pays for that convenience by discarding the collision signal. That is the trade-off.

The practical boundary is the record set, identified by at least zone, owner name, and type. Do not infer ownership from the hostname alone. An apex can legitimately hold several record types, while TXT record sets often contain several values. A controller that “owns the domain” in a database may still lack authority to replace every value at that name.

DNS standards also separate update conditions from update actions. RFC 2136 defines prerequisite forms such as name not in use, RRset does not exist, RRset exists, and value-dependent RRset existence. That is the clean mental model even when an API uses the friendlier labels create and upsert: the condition is part of the safety contract.

For an e-commerce cutover, use create for a new verification token or a new hostname whose absence is expected. Use upsert for a controller-owned traffic record after the desired value and rollback value have both been recorded. Use read-compare-update when a TXT set can contain unrelated values. A blind replacement there can remove mail policy or another team's verification token.

Pick the operation from ownership and retry semantics

Create fits a one-way ownership claim. The provisioner expects absence, so presence is a meaningful conflict. On failure, read the authoritative state and classify it: exact desired value means the previous attempt may have completed; a different value means stop. This preserves both retry safety and conflict detection without pretending one verb provides both.

Upsert fits reconciliation. A controller repeatedly declares the state of a record set it exclusively manages. The write may run after a queue redelivery, a process restart, or an uncertain timeout. Reapplying the same desired state is then correct. The cost is deliberate: an unexpected value can be overwritten, so the ownership manifest and audit log must carry the evidence that the overwrite was allowed.

Read-compare-update fits shared or manually managed sets. It is slower and still needs a provider-side conditional primitive to close the race between read and write. If no conditional update exists, serialize changes per record-set key and verify the result afterward, while recognizing that another writer outside that lock can still race. This option is operationally heavier. It is also honest about uncertainty.

The fourth option is no mutation. Stop when the zone cannot be identified unambiguously, when the current value is outside the controller's recorded ownership, or when the mail evidence is incomplete. A paused cutover is easier to recover than an overwritten SPF policy.

Stop means stop.

Implement a cutover as a verified state machine

The useful shape is a state machine, not a single setRecord() call. In words: prepare the desired and rollback snapshots; apply with a stated ownership policy; query authoritative DNS; observe delivery signals; promote; retain the rollback snapshot until the observation window closes.

Here is a compact Node.js model. The adapter is intentionally generic. It requires the infrastructure layer to expose create, upsert, and authoritative reads without tying the workflow to a particular service.

type RecordType = "A" | "AAAA" | "CNAME" | "TXT";

type DnsRecordSet = {
  zone: string;
  name: string;
  type: RecordType;
  ttl: number;
  values: readonly string[];
};

type ChangeMode = "create" | "upsert";

interface DnsAdapter {
  create(record: DnsRecordSet, changeId: string): Promise<void>;
  upsert(record: DnsRecordSet, changeId: string): Promise<void>;
  readAuthoritative(
    zone: string,
    name: string,
    type: RecordType,
  ): Promise<readonly string[]>;
}

type Ownership = {
  controller: string;
  recordKey: string;
  exclusive: boolean;
};

const normalized = (values: readonly string[]) =>
  [...values].map((value) => value.trim()).sort();

const sameValues = (left: readonly string[], right: readonly string[]) =>
  JSON.stringify(normalized(left)) === JSON.stringify(normalized(right));

async function applyAndVerify(
  dns: DnsAdapter,
  record: DnsRecordSet,
  ownership: Ownership,
  mode: ChangeMode,
  changeId: string,
): Promise<void> {
  const expectedKey = `${record.zone}|${record.name}|${record.type}`;
  if (ownership.recordKey !== expectedKey) {
    throw new Error(`Ownership mismatch for ${expectedKey}`);
  }
  if (mode === "upsert" && !ownership.exclusive) {
    throw new Error(`Upsert requires exclusive ownership of ${expectedKey}`);
  }

  try {
    await dns[mode](record, changeId);
  } catch (error) {
    const observed = await dns.readAuthoritative(
      record.zone,
      record.name,
      record.type,
    );
    if (!sameValues(observed, record.values)) throw error;
  }

  const observed = await dns.readAuthoritative(
    record.zone,
    record.name,
    record.type,
  );
  if (!sameValues(observed, record.values)) {
    throw new Error(`Authoritative verification failed for ${expectedKey}`);
  }
}
Enter fullscreen mode Exit fullscreen mode

Notice what the error path does. It does not assume that a thrown request means the mutation failed. It reads the authoritative result and accepts only an exact match. This handles the lost-response case for create without converting every create into a blind overwrite.

The example compares value sets rather than array order because DNS record ordering is not a useful ownership signal. Production normalization must remain type-aware. TXT quoting and escaping, fully qualified names, case rules, and provider response shapes should be normalized in the adapter, then tested with fixtures. Do not hide those transformations in the workflow.

Persist a compact evidence envelope for every attempt: change ID, record-set key, operation, desired value hash, prior value hash, actor, start time, provider acknowledgment, authoritative observations, and final decision. Keep raw sensitive token values out of general logs. Hashing supports correlation, but the secured rollback store still needs the actual prior values.

Use metrics that describe states rather than API traffic alone. Count conflicts by record type. Measure time from accepted mutation to authoritative match. Alert on cutovers stuck between “applied” and “verified,” and on any rollback whose authoritative result does not match the snapshot. A low mutation-error rate can coexist with broken delivery, so it is a weak release signal by itself.

Make deliverability the promotion gate

DNS verification answers “is the intended record visible from the authority?” It does not answer “will receivers accept and authenticate mail?” For a storefront hostname cutover that also changes sending or return-path records, promotion needs a separate mail gate.

SPF publishes authorization policy in DNS, DKIM attaches a cryptographic signature whose public key is retrieved through DNS, and DMARC tells receivers how to evaluate identifier alignment and provides aggregate reporting. DMARC policy discovery and reporting are not instant transaction acknowledgments. Aggregate reports describe receiver observations over reporting intervals, so they are evidence for staged promotion and later review rather than a synchronous response to one DNS write.

A practical rollout separates four checkpoints:

  1. Confirm the intended record set at the authoritative servers.
  2. Query through the recursive paths relevant to synthetic checks and record both answers and timestamps.
  3. Send controlled mail through the new path and inspect authentication results for SPF, DKIM, and DMARC alignment.
  4. Promote traffic only when the predefined evidence threshold is met; otherwise restore the recorded DNS snapshot and verify the rollback authoritatively.

The threshold is a team policy, not a universal DNS constant. Define it before the change. For example, the policy can require successful synthetic messages across the receiver classes the business actually uses, no unexpected authentication regressions, and an observation window tied to the previous TTL. Those are categories, not invented guarantees. The right sample size depends on normal mail volume and the risk of the hostname.

Keep the clocks visible. RFC 1035 defines TTL as the interval for which a resource record may be cached, so lowering a TTL immediately before a cutover does not erase copies cached under the earlier TTL. Wait for the former caching interval before relying on the lower value. After rollback, the same caching reality applies: authoritative correction can be immediate while recursive views remain mixed.

Two clocks matter.

This is why the rollback path must be prepared, not improvised. Store the exact previous record sets before mutation. Ensure the rollback writer has the same ownership checks as the forward writer. Then verify the restored state through authoritative queries and delivery probes. A dashboard should show “rollback requested,” “DNS restored,” and “delivery recovered” as separate states.

Limits that should stop automation

This workflow has real limitations. It is not suitable for record sets with multiple independent writers unless the DNS system offers a conditional update that every writer respects. Its audit envelope can show what the controller intended and observed, but it cannot prove legal ownership of a domain, eliminate stale recursive caches, or guarantee message acceptance. DNSSEC validation, delegated subzones, CNAME restrictions at particular names, and receiver-specific mail decisions remain separate concerns. Treat them as explicit preflight or observation checks where they apply. The operational trade-off is extra reads, stored snapshots, and a longer promotion path in exchange for conflict evidence and a tested rollback decision.

The generic state machine also assumes the DNS adapter preserves record-set semantics correctly. If an API models individual values instead, the adapter must prevent an update from dropping sibling values. If it cannot express a conditional write, document the remaining race rather than labeling the operation atomic.

The final decision is narrow: create when collision evidence matters; upsert when exclusive ownership makes convergence safe. Around that decision, authoritative verification and delivery evidence determine whether an e-commerce hostname cutover can advance or must roll back.

References

Top comments (0)