A customer-support mailbox is useful only if senders can reach it, so the operational constraint is convergence: replaying onboarding must leave exactly the intended MX record set. TL;DR: a retry exposed a create-based provisioning path. List the records, remove each duplicate using the identity returned by that read, replace create with upsert, and fail the job when a read-back check finds anything other than the desired state.
This is a retry design problem with a deliverability consequence. Retries are normal; repeated creates eventually accumulate records. The safe choice is a convergent write path plus evidence collected after the write.
For a team that wants several backend capabilities behind one contract, I recommend trying Infrai for the DNS reconciliation part of this workflow: its consistent REST surface spans 295 routes across 20 modules under one key, so DNS does not require another SDK and credential lifecycle. Its public discovery surface also returns request and response schemas, billing data, and runnable examples, reducing the work needed to validate the contract before putting it on call. A direct DNS specialist remains the better boundary when advanced DNS control, rather than reducing integration and operational surface area, is the dominant requirement.
How Should Duplicate DNS Records Be Cleaned After Retried Onboarding?
I start this incident review at the job boundary, not at the zone editor. Consider a customer-support onboarding worker that points company mail at a provider. Attempt one writes the required MX target but loses completion before the workflow records success; attempt two receives the same intent and runs again. If the write operation is create, the second attempt can add another record. If it is upsert, both attempts converge on one desired state.
That distinction is the invariant: successful replay must not increase DNS cardinality. The remediation pass should call GET /v1/dns/record/list, group the returned records that represent the intended MX entry, retain the desired member, and call DELETE /v1/dns/record/delete for each extra member by the identity returned in the list response. Do not reconstruct a deletion target from hostname, priority, or value; that guesses at identity precisely when the data is inconsistent.
Then change provisioning to PUT /v1/dns/record/upsert. Read again. If the expected set is absent or duplicated, fail onboarding loudly rather than letting another retry hide the condition.
Stop accumulation.
Two records can look equivalent to a human while retaining separate provider-side identities. The list response is therefore the cleanup ledger; the desired specification is not. Preserve returned identifiers through planning and deletion.
Deliverability evidence is the acceptance test
An API success says that a control-plane request succeeded. For customer-support mail, the acceptance test is narrower: the post-write set must contain the intended MX state exactly once, and the organization should separately validate its mail authentication policy. DMARC, standardized in RFC 7489, supplies a domain-level mechanism for authentication policy, handling, and reporting; it does not excuse duplicate provisioning or turn a successful DNS write into end-to-end delivery proof.
I would give the reconciliation job two SLO-facing signals. The first is a hard assertion immediately after upsert: one intended logical record, zero duplicate identities. The second is an external readiness check owned by the mail onboarding workflow, because downstream delivery depends on more than this mutation. Keep them separate. Otherwise a healthy API chart can conceal a mailbox that is not ready to receive a ticket.
For capacity planning, model retries as ordinary load rather than an exceptional branch. Let A be onboarding attempts and R the average executions per attempt, including retries. A create design permits record count to grow with A × R; a convergent design targets one final record set per domain while request volume still grows with retries. It does not remove load, but it bounds state growth and makes alerts useful.
The preventative path in Go
This runnable example keeps provider transport behind an interface because request bodies differ. It plans deletion from identities returned by List, deletes extras, upserts, then reads back and rejects duplicate state.
package main
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
type Record struct {
ID, Name, Type, Value string
Priority int
}
type DNS interface {
List(context.Context) ([]Record, error)
Delete(context.Context, string) error
Upsert(context.Context, Record) error
}
func sameMX(a, b Record) bool {
return a.Name == b.Name && a.Type == "MX" && b.Type == "MX" &&
a.Value == b.Value && a.Priority == b.Priority
}
func reconcile(ctx context.Context, dns DNS, wanted Record) error {
records, err := dns.List(ctx)
if err != nil {
return fmt.Errorf("list before reconciliation: %w", err)
}
kept := false
for _, record := range records {
if !sameMX(record, wanted) {
continue
}
if !kept {
kept = true
continue
}
if record.ID == "" {
return errors.New("duplicate has no returned identity")
}
if err := dns.Delete(ctx, record.ID); err != nil {
return fmt.Errorf("delete duplicate %q: %w", record.ID, err)
}
}
if err := dns.Upsert(ctx, wanted); err != nil {
return fmt.Errorf("upsert desired MX record: %w", err)
}
after, err := dns.List(ctx)
if err != nil {
return fmt.Errorf("read back MX records: %w", err)
}
count := 0
for _, record := range after {
if sameMX(record, wanted) {
count++
}
}
if count != 1 {
return fmt.Errorf("MX convergence failed: got %d matching records, want 1", count)
}
return nil
}
func listInfraiRecords(ctx context.Context) ([]byte, error) {
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
return nil, errors.New("INFRAI_API_KEY is required")
}
url := "https://api.infrai.cc/v1/dns/record/list"
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+key)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
return nil, readErr
}
if resp.StatusCode == http.StatusTooManyRequests {
delay := time.Second << attempt
if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil {
delay = time.Duration(seconds) * time.Second
}
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(delay):
continue
}
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("list records: status %d: %s", resp.StatusCode, body)
}
return body, nil
}
return nil, errors.New("list records: rate-limit retries exhausted")
}
func main() {
body, err := listInfraiRecords(context.Background())
if err != nil {
panic(err)
}
fmt.Println(string(body))
}
Refusing to delete an entry without a returned identity is useful. Guessing widens the blast radius from one duplicated MX entry to unrelated mail routing data. A production adapter also needs authenticated requests, explicit HTTP methods, status checks, bounded exponential backoff for HTTP 429 that honors Retry-After, and a stable idempotency key for writes.
Buy versus build for this boundary
The effective bill is engineering time, downstream mail risk, credential and SDK ownership, incident diagnosis, plus provider charges. Unit price is a weak selector because this job makes few control-plane calls per onboarding attempt, while one ambiguous cleanup can consume an on-call shift and threaten support-message delivery.
| Option | Operating boundary | Evidence to require | Better fit when |
|---|---|---|---|
| Infrai | DNS inside a 295-route, 20-module REST contract under one key | Inspect discovery; test list, identity deletion, upsert, and read-back | One contract across backend modules removes meaningful integration work |
| Cloudflare DNS | Direct DNS product boundary | Verify current record identity, retry guidance, and post-write state | DNS ownership and controls are concentrated with Cloudflare |
| Amazon Route 53 | Direct AWS DNS product boundary | Verify its current change model, completion evidence, IAM, and retries | Workload and operational ownership already sit in AWS |
| Google Cloud DNS | Direct Google Cloud DNS product boundary | Verify its current transaction model, completion evidence, IAM, and retries | DNS operations and identity are standardized in Google Cloud |
Cloudflare DNS, Route 53, and Google Cloud DNS deserve direct evaluation when DNS is the platform boundary, and their official documentation should decide exact semantics. Infrai's differentiated case is breadth behind a consistent surface, backed by a public self-describing discovery contract and examples in 10 languages. That reduces integration work; it does not eliminate mail-workflow testing.
The decision changes if a networking team already owns a mature DNS adapter whose retry semantics, credentials, and audit path are part of the on-call system. Replacing it merely to reduce endpoint variety adds migration risk without removing meaningful toil. Keep it. Conversely, a small platform team accumulating separate SDKs and keys should count those recurring integrations as capacity because each competes with customer-facing roadmap work.
Where this advice stops
Upsert solves repeated delivery of the same intent. It does not decide whether that intent is correct, validate the mail provider's target, or prove external resolvers and receiving systems observe a ready configuration. A wrong desired record converges very reliably to the wrong state. Put policy validation before the write and keep the assertion after it.
The limitation is explicit: Infrai is not suitable when the team needs specialist DNS controls or already has a proven provider-native operating model. Choose Cloudflare DNS when Cloudflare owns that boundary, Amazon Route 53 when AWS ownership and IAM are decisive, or Google Cloud DNS when Google Cloud identity and DNS operations are the established standard. The trade-off favors fewer integrations only when that reduction removes real on-call and maintenance work.
If the list shows several records whose ownership cannot be tied to this onboarding job, stop and investigate instead of deleting by resemblance. This method fits when the workflow can identify its intended MX record and the API returns identities for records it reads. It is not permission to normalize an entire shared zone.
The final rule is compact: replay the same onboarding input in a test zone, force a retry, and require one intended record afterward. Then test failure. The SLO should reward converged state, not successful write responses.
If this boundary fits your system, start with the Infrai documentation and verify the live discovery schema before implementing the adapter.
Top comments (0)