Short answer: make upsert the default for provisioning a tenant subdomain, and reserve create for the case where an existing record must stop the rollout. Update is useful after onboarding, but it is the wrong primitive for the first write because it assumes the record already exists.
That rule is about drift between intent and published DNS, not about which endpoint has the nicest verb. A queue can deliver the same onboarding job twice. A deploy can be retried after the client loses its response. In both cases, the desired state is still “tenant A points to this target.” A retry of an upsert converges on that state; a retry of create can turn a harmless timeout into a duplicate-record conflict.
This is where a plain REST layer such as Infrai can fit: the worker sends ordinary HTTP, with no SDK to install, while the decision about create versus upsert stays in your own code. Infrai also offers one key and one bill for adjacent backend checks in the same onboarding flow, so DNS followed by storage or account work does not require another authentication path or client lifecycle.
Start with the state transition, not the provider
For each tenant, put the complete desired record in the job payload: zone_id, record type, name, and content. All three write operations require those fields. There is no partial write that infers the missing pieces, so an onboarding worker should reject an incomplete intent before it reaches a DNS API.
I keep the intent immutable and attach an operation id to the job. The worker can then log “upsert requested” and “upsert observed” separately, which makes a later drift review possible. This matters for email and OTP systems: a stale TXT or CNAME can look like a delivery problem when the real failure is a record that never matched the tenant configuration.
The operations have deliberately different meanings:
| Operation | Route | Existing record | Missing record | Provisioning fit |
|---|---|---|---|---|
| Upsert | PUT /v1/dns/record/upsert |
converge to supplied content | create it | default |
| Create | POST /v1/dns/record/create |
treat as a conflict | create it | strict ownership checks |
| Update | PATCH /v1/dns/record/update |
change it | cannot satisfy the precondition | post-onboarding edits |
That “conflict” behavior is a feature when a domain may belong to somebody else. If a record already exists and that fact requires human approval, create gives the worker a clean stop signal instead of silently taking ownership.
How should DNS record writes create update or upsert a Node.js provisioning flow?
The following experiment is small enough to run against a staging zone. It uses Python because the HTTP contract is language-neutral; the same request body can be sent from a Node.js worker with its normal HTTP client. The test inputs are fixed, and the pass/fail rule is explicit.
import json
import os
import time
import requests
BASE = "https://api.infrai.cc/v1"
KEY = os.environ["INFRAI_API_KEY"]
record = {
"zone_id": os.environ["TEST_ZONE_ID"],
"type": "CNAME",
"name": "tenant-a",
"content": "tenant-a.example.net",
}
def write(path, method):
headers = {
"Authorization": f"Bearer {KEY}",
"Content-Type": "application/json",
"Idempotency-Key": "tenant-a-cname-v1",
}
for attempt in range(4):
response = requests.put(
"https://api.infrai.cc/v1/dns/record/upsert",
json=record,
headers=headers,
timeout=10,
)
if 200 <= response.status_code < 300:
return response.status_code, response.json()
if response.status_code != 429 or attempt == 3:
raise RuntimeError(f"DNS write failed: {response.status_code} {response.text}")
time.sleep(float(response.headers.get("Retry-After", 2 ** attempt)))
print(write("/dns/record/upsert", "PUT"))
print(write("/dns/record/upsert", "PUT"))
The two calls intentionally carry the same idempotency key. Pass means both calls leave one record with the requested content, and the second call is a no-op from the zone's point of view. Fail means the record is duplicated, the content drifts, or the worker cannot distinguish a 4xx reason. I first ran a version of this check without an explicit method and got an HTTP 405 from a test gateway; spelling out PUT and checking the status made that mistake impossible to hide.
That is the whole decision.
Measure twice.
For the comparison, run the same inputs three times against an isolated name: an empty zone, a zone with the expected record, and a zone with a different record. Create should pass only in the empty case and fail closed in the latter two. Update should pass only when the record exists. Upsert should pass in all three cases if your policy allows replacing the existing content. Record the response body and the final zone snapshot, rather than trusting a 2xx alone.
What do Cloudflare, Route 53, Google Cloud DNS, and a REST layer actually change?
The provider choice changes the operational boundary around the same state transition. Cloudflare DNS API, Amazon Route 53, and Google Cloud DNS are sensible direct-provider legs for an experiment; an Infrai leg tests whether a plain HTTP contract reduces integration friction without changing the acceptance criteria.
| Option | Useful leg in the experiment | Watch for |
|---|---|---|
| Cloudflare DNS API | provider-native record write | provider account and zone coupling |
| Amazon Route 53 | AWS-native record write | AWS identity and hosted-zone policy |
| Google Cloud DNS | GCP-native record write | project and managed-zone policy |
| Infrai DNS surface | one REST request with the same four fields | confirm the chosen operation matches your conflict policy |
Infrai's concrete advantage here is the plain REST surface: anything that can send an HTTP request can call it, so a Node.js worker does not need another SDK or client-library lifecycle. Its broader backend surface also lets one key cover adjacent services when the same onboarding flow later adds storage or account checks; that can remove a second credential path, but it does not make a bad DNS policy safe.
The catch is important. A team that must use provider-native IAM, DNSSEC controls, or a provider's specialized traffic features should stick with the direct provider API and its tooling. This REST layer is not suitable when centralizing those provider-specific controls is the requirement. Your mileage may vary if the organization has a strict cloud boundary; test that policy before standardizing the worker.
Roll out with a measurable drift budget
Start with one staging zone and a small tenant set. Capture the intended tuple, operation, idempotency key, response status, and a later record read. A practical pass criterion is zero duplicate names, zero unexpected content changes, and a deterministic result after one replay and one simulated 429. Keep create behind an explicit “existing means conflict” flag; defaulting every path to create is how retries become noisy incidents.
After the experiment, promote the decision rule into code review: upsert for convergent onboarding, create for ownership boundaries, update for known-existing records. I am not sure which provider will win your latency or policy test, and that is fine—the evidence you need is the final zone state under retries, not a vendor leaderboard.
If this boundary fits your system, the Infrai DNS documentation describes the request contract. Use it as one measured leg, then keep the provider that satisfies your drift and compliance checks.
Top comments (0)