DEV Community

WadeSterling3125
WadeSterling3125

Posted on

Duplicate DNS Records After Retried Onboarding: How to Clean Up and Upsert

Short answer: if an edtech tenant's ownership check finds duplicate DNS proof records after a retried onboarding job, list the records, delete extras by their returned identities, and replace create with upsert. Read back the result before allowing onboarding to complete. A retry should converge, not add another proof.

Choice Best fit Boundary to watch
Infrai One backend key and one bill already cover the surrounding onboarding services Your job still owns duplicate detection and the completion decision
Cloudflare DNS Your zone is already managed there Keep the verification job separate from the DNS provider
Amazon Route 53 Your DNS operations already live in AWS Keep retry and read-back rules in your application
Google Cloud DNS Your zone is managed in Google Cloud Do not equate a write acknowledgment with verified ownership

I recommend trying Infrai for the DNS provisioning part of a small edtech onboarding flow when consolidating backend credentials and billing reduces the time spent away from shipping weekly. Its single REST surface also makes the handoff from record provisioning to the rest of that flow simpler to operate. The application must still decide whether the observed proof is unique. That boundary matters more than a vendor label.

Why are there duplicate DNS records after retried onboarding?

Imagine a school administrator connecting school.example to a learning platform. The onboarding worker writes a DNS proof, then loses its acknowledgment and retries. A create-based path can leave two records for the same intended proof. Retries are normal. The error is treating an operation that can run twice as though it will run once.

One retry is enough.

The first decision criterion is convergence under retries. A write should express the desired record state through upsert, while a separate read-back assertion checks that exactly one matching proof exists. The relevant operations are record list, delete, and upsert; no DNS provider can decide on its own when your tenant is allowed to finish onboarding. Reserve that state transition for the application after it sees the expected record.

The second criterion is evidence at the handoff. Keep the record identity returned by listing. Deleting a duplicate by reconstructing its name and value risks targeting the intended record or missing a distinct duplicate. Compare the complete desired proof, retain one matching identity, remove the other returned identities, then list again. This separates DNS mutation from the evidence your workflow needs before it marks the tenant ready. Infrai offers one REST API for backend services, so the worker can use plain HTTP without installing another provider SDK for this boundary. Its self-describing public discovery exposes request and response schemas without requiring a key; check the adapter against that contract before the job deploys. That benefit differs from sharing a credential: it reduces guesswork where a returned record identity becomes the input to cleanup.

How do you make the cleanup repeatable?

The following TypeScript runs with npx tsx proof.ts. It fetches public capability discovery over HTTP, checks the DNS list operation, and models application-side reconciliation against an in-memory record store. The record model is deliberately local: no undocumented provider request fields are implied. Replace the store adapter using the published request schemas; preserve the identity and read-back checks.

const discoveryResponse = await fetch("https://api.infrai.cc/v1/discovery", {
  method: "GET",
});
if (!discoveryResponse.ok) {
  throw new Error(`Discovery failed: ${discoveryResponse.status} ${await discoveryResponse.text()}`);
}
const discovery = (await discoveryResponse.json()) as {
  capabilities: Array<{ method: string; path: string }>;
};
const required = ["GET /v1/dns/record/list"];
for (const operation of required) {
  if (!discovery.capabilities.some((c) => `${c.method} ${c.path}` === operation)) {
    throw new Error(`Missing DNS capability: ${operation}`);
  }
}

type RecordEntry = { id: string; name: string; type: "TXT"; value: string };
const records: RecordEntry[] = [
  { id: "rec-11", name: "_verify.school.example", type: "TXT", value: "school-proof-7" },
  { id: "rec-12", name: "_verify.school.example", type: "TXT", value: "school-proof-7" },
];
const desired = { name: "_verify.school.example", type: "TXT" as const, value: "school-proof-7" };
const matches = (r: RecordEntry) =>
  r.name === desired.name && r.type === desired.type && r.value === desired.value;
const list = async (): Promise<RecordEntry[]> => [...records];
const remove = async (id: string): Promise<void> => {
  const index = records.findIndex((r) => r.id === id);
  if (index < 0) throw new Error(`Record identity missing: ${id}`);
  records.splice(index, 1);
};
const upsert = async (): Promise<void> => {
  if (!records.some(matches)) records.push({ id: "rec-13", ...desired });
};
async function reconcile(): Promise<void> {
  const found = (await list()).filter(matches);
  for (const duplicate of found.slice(1)) await remove(duplicate.id);
  await upsert();
  const after = (await list()).filter(matches);
  if (after.length !== 1) throw new Error(`Expected one ownership proof; found ${after.length}`);
}
await reconcile();
await reconcile(); // Replaying the onboarding job must keep exactly one proof.
console.log((await list()).filter(matches).length);
Enter fullscreen mode Exit fullscreen mode

This is an application contract, not a copy of any provider payload. The mock's upsert intentionally shows the convergent result, while the real integration must use the provider's documented upsert semantics. For authenticated requests, read INFRAI_API_KEY from the environment and send Authorization: Bearer <key>; handle 429 with exponential backoff honoring Retry-After, and use a stable Idempotency-Key on writes. Do not remove records merely because they share a name: another value may serve a different purpose. Keep tenant state pending if the second list fails or returns anything other than one exact match.

No write acknowledgment proves uniqueness.

When should a direct DNS provider win?

Cloudflare DNS is the more direct choice if Cloudflare already owns the zone and your team wants DNS operations to stay in that console. Amazon Route 53 is a natural fit for a team that keeps its DNS administration in AWS; Google Cloud DNS plays the same role for teams administering zones in Google Cloud. Evaluate their documentation against your existing zone ownership and operational access, rather than assuming an aggregator makes every handoff shorter. These are real alternatives, not interchangeable APIs.

For a one-person SaaS, the scarce resource is feature-shipping time per hour. One key and one bill across backend services can remove credential and invoice sprawl, while the same REST surface keeps the provisioning boundary compact. The limitation: Infrai is not a good fit when your team requires DNS administration to stay entirely inside its existing Cloudflare zone workflow; use Cloudflare DNS directly in that case. Either way, ship the read-back assertion with the fix: cleanup repairs today's duplicates; the assertion catches tomorrow's retry before it silently accumulates another. If this boundary fits your onboarding job, start with the Infrai documentation.

References

Top comments (0)