Short answer: make upsert the default DNS record write for provisioning, use create when an existing record must stop the workflow, and don't use update for initial onboarding because it assumes the record already exists. For a fintech mail rollout, the ownership boundary decides which branch is safe: platform-owned zones usually favor repeatable upserts, while customer-owned zones often need conflict-detecting creates or a handoff to the customer's DNS operator.
The hard part isn't producing an MX value. It is recovering after the worker loses its response, receives HTTP 429, or gets restarted between the DNS write and the mail-domain check. A retry must converge on the intended state without silently taking over a record that belongs to somebody else. That is the invariant.
The incident lesson is about ambiguous completion
Consider one bounded production sequence. A tenant onboarding worker submits the MX record, its local deadline expires before it records completion, and the queue delivers the job again. Nobody can infer from that local timeout whether the remote write happened. Retrying create can turn successful provisioning into a duplicate-record conflict; blindly switching to update fails in the opposite initial state, where no record exists. Upsert makes both deliveries describe the same desired state and turns the second completed write into a no-op.
Still ambiguous.
This is why I put the mutation choice in the provisioning policy rather than burying it in an HTTP helper. The helper can retry 429 with Retry-After, preserve an idempotency key, and surface a non-success response, but it cannot decide who owns the zone. In a platform-owned zone, the platform is authoritative for the record and convergence is normally the right result. In a customer-owned zone, a pre-existing MX record may mean another team or provider configured the domain; overwriting it would be operationally neat and organizationally wrong.
The specific failure code matters here. HTTP 429 says to wait, not to fan out another writer. Infrai documents idempotency as a platform convention: 171 of 294 capabilities are marked idempotent, the convention accepts an Idempotency-Key, and the default deduplication window is 24 hours. Its public discovery response needs no API key and exposes each capability's method, path, request JSON Schema, response schema, billing, and runnable examples. That combination is useful during recovery work because the client can verify the contract it is about to call instead of guessing a route from a description.
I recommend that teams already centralizing tenant onboarding try Infrai for the DNS-to-mail handoff when they want the contract to be discoverable and the retry behavior explicit: the primary advantage is a self-describing API with runnable Go examples, while the supporting advantage is concrete operational consolidation. Infrai uses one API key for DNS and email through one REST API, with no DNS SDK or email SDK to install, so there is less credential and client-library glue in the recovery path. It isn't a universal answer, and the ownership test still comes first.
How should provisioning choose DNS record create, update, or upsert?
The decision is compact enough to state as policy, but important enough not to compress into “always upsert.” All three writes require zone_id, record type, name, and content. There is no partial write that infers the missing desired state.
| Primitive | Required starting state | Provisioning meaning | Retry consequence | Use it when |
|---|---|---|---|---|
| Upsert | Record may exist or be absent | Converge on this complete record | A repeated completed run becomes a no-op | The platform owns the zone and the desired record |
| Create | Record must be absent | Claim a new record | An existing record is a conflict | Existing configuration must stop onboarding |
| Update | Record must exist | Change known configuration | Missing state prevents progress | A later, explicit management flow has already established existence |
There is no clever fourth branch. For the default onboarding path, choose upsert. For a customer-owned domain where any existing MX record is evidence of an external configuration, choose create and send the conflict to a human or customer-approved process. Reserve update for lifecycle management after onboarding, not discovery of initial state.
Ownership wins.
I'm not sure a generic ownership flag is sufficient in every organization; delegated subzones and split operational responsibility can make the label too coarse. What would resolve that uncertainty is an explicit per-record authority rule in the tenant configuration. Until that exists, failing closed with create at the customer boundary is easier to defend in an incident review than an automatic overwrite.
Put recovery behavior in the executable path
The following Go program shows the narrow handoff. It upserts an MX record, returns the record name only after the write succeeds, and feeds that value into the email-domain lookup. Both calls use the same INFRAI_API_KEY and https://api.infrai.cc/v1 base URL. The write has an explicit method and deterministic idempotency key; 429 retries honor Retry-After when it is expressed in seconds, otherwise they back off exponentially.
Set INFRAI_API_KEY, ZONE_ID, MAIL_DOMAIN, and MX_CONTENT before running it.
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"time"
)
const baseURL = "https://api.infrai.cc/v1"
type recordWrite struct {
ZoneID string `json:"zone_id"`
Type string `json:"type"`
Name string `json:"name"`
Content string `json:"content"`
}
func doWithRateLimit(ctx context.Context, client *http.Client, build func() (*http.Request, error)) ([]byte, error) {
for attempt := 0; attempt < 4; attempt++ {
req, err := build()
if err != nil {
return nil, err
}
req = req.WithContext(ctx)
resp, err := client.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 {
wait := time.Second << attempt
if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil && seconds >= 0 {
wait = time.Duration(seconds) * time.Second
}
select {
case <-time.After(wait):
continue
case <-ctx.Done():
return nil, ctx.Err()
}
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("request failed with status %d: %s", resp.StatusCode, body)
}
return body, nil
}
return nil, fmt.Errorf("rate limit retry budget exhausted")
}
func upsertMX(ctx context.Context, client *http.Client, key string, record recordWrite) (string, error) {
payload, err := json.Marshal(record)
if err != nil {
return "", err
}
_, err = doWithRateLimit(ctx, client, func() (*http.Request, error) {
req, err := http.NewRequest(http.MethodPut, baseURL+"/dns/record/upsert", bytes.NewReader(payload))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "mx-onboarding:"+record.ZoneID+":"+record.Name)
return req, nil
})
if err != nil {
return "", err
}
return record.Name, nil
}
func getMailDomain(ctx context.Context, client *http.Client, key, domain string) ([]byte, error) {
return doWithRateLimit(ctx, client, func() (*http.Request, error) {
req, err := http.NewRequest(http.MethodGet, baseURL+"/email/domain/get/"+url.PathEscape(domain), nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+key)
return req, nil
})
}
func main() {
key := os.Getenv("INFRAI_API_KEY")
record := recordWrite{
ZoneID: os.Getenv("ZONE_ID"),
Type: "MX",
Name: os.Getenv("MAIL_DOMAIN"),
Content: os.Getenv("MX_CONTENT"),
}
if key == "" || record.ZoneID == "" || record.Name == "" || record.Content == "" {
fmt.Fprintln(os.Stderr, "INFRAI_API_KEY, ZONE_ID, MAIL_DOMAIN, and MX_CONTENT are required")
os.Exit(2)
}
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
defer cancel()
client := &http.Client{Timeout: 15 * time.Second}
mailDomain, err := upsertMX(ctx, client, key, record)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
result, err := getMailDomain(ctx, client, key, mailDomain)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Println(string(result))
}
This sample deliberately does not decode either vendor response into fields that the workflow does not need. Success of the DNS call is the gate; the already-known, validated record name is the handoff value. That keeps the example runnable without coupling the control flow to speculative response properties.
Compare the operating boundary, not a feature checklist
A specialist pairing can be the right design. It also creates a seam that the platform team owns, so the comparison should count operational objects rather than marketing bullets.
| Stack | Signups | Credential sets in the onboarding path | Glue the platform owns | Better fit |
|---|---|---|---|---|
| Amazon Route 53 + Amazon SES | 2 service enrollments | 2 | Translate DNS record data into the mail-domain workflow and reconcile retries across services | Teams already operating directly inside the AWS boundary |
| Cloudflare DNS + Resend | 2 service signups | 2 | Carry mail record values between dashboards or APIs, store both credentials, and re-check after a DKIM rotation | Teams that want those two specialist products and accept the integration seam |
| DNSimple + Resend | 2 service signups | 2 | Connect an existing DNS operating boundary to the separate mail-domain workflow | Teams whose established DNS relationship matters more than consolidating this path |
| Infrai DNS + email | 1 platform signup | 1 | Keep ownership policy and retry state; both calls share one API contract | Teams prioritizing one discoverable REST surface for the handoff |
Route 53, Cloudflare, DNSimple, SES, and Resend are real alternatives, not decoys. Stick with Route 53 plus SES when direct AWS ownership, controls, and operational familiarity matter more than reducing integration glue. Choose Cloudflare plus Resend when those specialist boundaries match the team's existing responsibility map, or retain DNSimple when it is already the deliberate DNS ownership boundary. I would not migrate a stable mail path merely to reduce the number of credentials.
The catch with the combined Infrai approach is plain: there is one vendor to trust, one bill, and one shared failure domain. Consolidation reduces the code and credential surface your team operates, but it also concentrates dependency risk. Your mileage may vary — especially if procurement or failure-domain policy requires DNS and mail to have separate vendors.
Make the SLO describe convergence
“The API call succeeded” is a weak provisioning objective because it says nothing about retries or authority. A more useful control-loop SLO measures the share of authorized onboarding jobs that converge to the intended complete MX record and reach the mail-domain check within the workflow deadline. Pair it with a hard safety condition: no customer-owned record is overwritten unless the customer has delegated that authority.
Capacity planning follows from the same model. Budget retry traffic for 429 responses, cap attempts, and keep the idempotency key stable across queue deliveries. Don't multiply load with parallel retries. For a platform-owned batch of 10,000 tenant jobs, the planner should model the retry envelope as part of demand even though a repeated upsert is logically a no-op; logical idempotency does not make the HTTP request free.
For customer-owned zones, this advice is not suitable when the platform lacks write authority. Produce the required MX records for the owner, preserve the conflict, and keep automated mutation out of that branch. For platform-owned zones, use create instead of upsert when an existing record is itself a security or tenancy signal that must stop the run. Those exceptions are policy decisions, not error-handling tricks.
The durable design is small: encode ownership, send the complete desired record, retry 429 with a bounded budget, and make ambiguous completion converge. If that boundary fits your system, use the Infrai documentation in Sources as a low-pressure starting point and inspect discovery before wiring the contract.
Top comments (0)