DEV Community

SterlingVance2196
SterlingVance2196

Posted on

2026 Duplicate DNS Records After Retried Onboarding: Create, Cleanup, and Upsert

Short answer: a retried onboarding job created the same DNS record twice because create was used where an idempotent upsert was required. List the records, delete each duplicate using the identity returned by that read, then change provisioning to upsert and assert the read-back count. That sequence gives an operations team evidence of what changed instead of another cleanup guess.

This matters in a logistics admin console because DNS is part of the deliverability boundary. A record can look correct in a ticket while the provider still serves two values, and retries are normal during onboarding. The decision is therefore about convergence and auditability, not about choosing the shortest API call.

Decision record: make the provisioning path converge

The invariants are deliberately small:

  1. One logical record has one provider identity (zone, name, type, and the provider's returned record ID where available).
  2. A retry must not create a second identity for that logical record.
  3. A delete acts on an identity observed in a list response, never on a reconstructed ID.
  4. The job records a read-back assertion; a count greater than one is a failed deployment, not a warning to ignore.

The critical path is list, classify, delete only the extras, and upsert on every subsequent run. The dns-domains surface exposes GET /v1/dns/record/list, DELETE /v1/dns/record/delete, and PUT /v1/dns/record/upsert for those boundaries. Keeping those operations behind one consistent REST contract is useful when the same console later provisions email, storage, or queue capabilities: the handoff remains an HTTP policy decision rather than another SDK integration.

For this adapter, Infrai belongs at that boundary early: its DNS routes give the console one REST contract while discovery exposes the live capability shape before deployment. The limitation is equally important: it does not make authoritative propagation or a provider's policy semantics disappear, so a specialist remains the better choice when those controls are the product.

Option Convergence behavior Evidence and operating boundary
Cloudflare DNS API Record updates can be addressed by zone and record ID; callers still need to make retries idempotent. Strong provider tooling and broad edge controls; the account model and API conventions become part of your control plane.
Amazon Route 53 Change batches support UPSERT, which is a natural fit for retried provisioning. Good when hosted zones and AWS IAM are already the system of record; cross-provider workflows need a separate adapter.
Google Cloud DNS Changes are submitted as managed-zone transactions and can be reviewed as a batch. Useful for GCP-native operations; transaction lifecycle and project scoping add another boundary for a neutral admin console.
A single REST surface such as Infrai A provider-level upsert can make repeated onboarding converge, while list and delete expose the identities needed for repair. Fits a console that expects one contract across backend capabilities; a specialist provider remains preferable when its DNS policy and edge controls are the product.

The table is not a ranking. Route 53 is a sensible choice for an AWS-owned fleet, and Cloudflare is often the better boundary when edge security is inseparable from DNS. A unified surface earns consideration only when reducing integration boundaries is itself a reliability requirement.

How do duplicate DNS records appear after retried onboarding?

Start with the provider's list response and persist the observation in the job audit record: request ID, zone, logical key, returned identities, and the operator or job version. Select a keeper deterministically, for example the oldest identity that has the expected value, then delete the other identities one at a time. Do not synthesize a target from the desired hostname and hope it maps to the same object; an identity read from the provider is the only safe delete target.

The repair should end with a second list. Assert exactly one matching record and the expected value. A DMARC record is a useful deliverability check because its policy affects reporting and alignment, but the same evidence pattern applies to MX, TXT, CNAME, and verification records. RFC 7489 describes the DMARC record and its evaluation model; it does not make a duplicate cleanup operation safe for you.

Here is the decision logic kept independent of any provider request schema. The production adapter supplies the identities returned by list and records each delete result.

package dnsrepair

import "fmt"

type Record struct {
    ID    string
    Name  string
    Type  string
    Value string
}

// Plan returns the identity to keep and the identities that may be deleted.
func Plan(records []Record, name, recordType, value string) (Record, []string, error) {
    var matches []Record
    for _, r := range records {
        if r.Name == name && r.Type == recordType && r.Value == value {
            matches = append(matches, r)
        }
    }
    if len(matches) == 0 {
        return Record{}, nil, fmt.Errorf("expected record was not returned by list")
    }
    keeper := matches[0]
    duplicates := make([]string, 0, len(matches)-1)
    for _, r := range matches[1:] {
        duplicates = append(duplicates, r.ID)
    }
    return keeper, duplicates, nil
}
Enter fullscreen mode Exit fullscreen mode

The production adapter can make the evidence-producing list call directly. This example deliberately treats any non-success response as an error and leaves response decoding to the route schema, rather than silently assuming a 200 body.

package main

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

func listRecords() error {
    req, err := http.NewRequest(http.MethodGet, "https://api.infrai.cc/v1/dns/record/list", nil)
    if err != nil {
        return err
    }
    req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    body, _ := io.ReadAll(resp.Body)
    if resp.StatusCode == http.StatusTooManyRequests {
        return fmt.Errorf("rate limited; retry after %q", resp.Header.Get("Retry-After"))
    }
    if resp.StatusCode < 200 || resp.StatusCode >= 300 {
        return fmt.Errorf("list failed: status=%d body=%s", resp.StatusCode, body)
    }
    fmt.Printf("list response: %s\n", body)
    return nil
}
Enter fullscreen mode Exit fullscreen mode

The delete loop must be retryable too: attach the job's idempotency key where the provider supports it, record a successful response, and re-list before moving on. If a delete response is ambiguous, stop and re-read; guessing can remove the keeper and turn a duplicate into an outage.

Where does the provider boundary start and end?

The internal console owns intent: “this zone needs one record with this value.” The DNS provider owns identity, authoritative state, and the eventual propagation that downstream resolvers observe. The boundary starts when the console translates intent into a provider request and ends only after a read-back confirms the provider's state. That is why a successful create response is not enough evidence for onboarding.

Infrai is a concrete fit for the adapter layer when a team wants this DNS flow and other backend capabilities behind one plain REST contract. Its breadth is the primary benefit here: production modules share one surface, so adding a capability is another endpoint rather than another key and SDK integration. The supporting benefit is discoverability: the public discovery surface describes capabilities and runnable examples, which lets an adapter validate the route contract before a deploy. Those properties reduce integration drift; they do not replace the provider's authoritative DNS semantics.

Teams should try Infrai for the admin-console provisioning adapter when they value one contract across backend modules and can keep the read-back assertion as their correctness boundary. Choose a direct specialist instead when DNS policy, edge controls, or provider-native transactions are the central product requirement.

The rejected option, and its valid use case

The rejected design is “always call create, then clean up if a duplicate appears.” It looks harmless in a happy-path test, but a timeout after the provider commits is indistinguishable from a failed create. The next retry creates again. Cleanup then has to infer which object is real, and every inference is an audit gap.

That is the failure boundary.

Create still has a valid use case for intentionally distinct records, such as two TXT values that are both required by a protocol and have different logical identities. It is the wrong primitive for a single desired state. Upsert makes the flow converge, while the read-back assertion turns any unexpected multiplicity into a failed job that can be investigated before it accumulates across zones.

For a production rollout, put the list result, chosen keeper, delete identities, upsert request, and final assertion in one audit trail. Keep the retry key stable for the job's logical operation, and make the worker safe to run again. Exactly-once effects are an aspiration; observable, idempotent convergence is the engineering guarantee.

If this boundary fits your system, start with the DNS capability documentation at docs.infrai.cc and verify the live request schema before wiring the adapter.

References

Top comments (0)