Checking propagation#
When a change is live, how to know, and what to do before you press verify.
What a successful write means#
A write (set_dns2, PUT, DELETE) returns only after the change is saved at the authoritative nameservers: the servers the rest of the internet asks about your host.
- Public resolvers (the ones Google, Microsoft and everybody else use) usually see a new record within one to two minutes.
- If you replaced a record, a resolver that cached the old one can keep serving it until that record's TTL runs out (usually 3600 seconds;
get_dnsshows it). - If a resolver asked for the name before the record existed, it may remember "no such record" for a while. That is why you don't verify until the record is live.
Ask before you verify#
- Write the record.
- Poll
dns_status(Dynadot style) orGET /api/v1/dns/status/{domain}(Spaceship style) with the record's name, type and value, every 10 to 15 seconds. - When it says live, ask Google or Microsoft to verify.
- If verification still fails, wait a few minutes and try again. Don't write the record again: it is already there.
dns_status asks each authoritative nameserver directly, without any cache in between, and says yes only when every one of them answers with the record.
It exists in two styles, Dynadot and Spaceship. Writing in Cloudflare, GoDaddy, Porkbun or Namecheap style? Call the Dynadot-style dns_status below with the same key: it only needs key.
Dynadot style#
curl https://api.maildns.io/api3.json \
--data-urlencode key=KEY \
--data-urlencode command=dns_status \
--data-urlencode domain=advisor.example.com \
--data-urlencode subdomain=@ \
--data-urlencode record_type=txt \
--data-urlencode value=google-site-verification=3J9tq0iV8pTt1yW5zKc2mE7aR4bN6hD0sF1gL8xQ{
"DnsStatusResponse": {
"ResponseCode": 0,
"Status": "success",
"Records": [
{
"Name": "advisor.example.com",
"RecordType": "TXT",
"Value": "google-site-verification=3J9tq0iV8pTt1yW5zKc2mE7aR4bN6hD0sF1gL8xQ",
"Live": "yes",
"Servers": {"ns1.example.net": "yes", "ns2.example.net": "yes"}
}
]
}
}subdomain=@ is the host itself; for DKIM use subdomain=google._domainkey, for DMARC subdomain=_dmarc. Full parameters: dns_status.
Spaceship style#
curl -G https://api.maildns.io/api/v1/dns/status/advisor.example.com \
-H 'X-API-Key: KEY' -H 'X-API-Secret: SECRET' \
--data-urlencode name=_dmarc \
--data-urlencode type=TXT \
--data-urlencode 'value=v=DMARC1; p=quarantine'{
"items": [
{
"name": "_dmarc.advisor.example.com",
"type": "TXT",
"value": "v=DMARC1; p=quarantine",
"live": false,
"servers": {"ns1.example.net": true, "ns2.example.net": false}
}
]
}Full parameters: Check records.
A polling loop#
import os
import time
import requests
API = "https://api.maildns.io/api3.json"
KEY = os.environ["MAILDNS_KEY"]
def is_live(host, name, rtype, value):
"""name is relative to host: "@" for the host itself, "_dmarc", "google._domainkey"."""
reply = requests.post(API, timeout=90, data={
"key": KEY, "command": "dns_status", "domain": host,
"subdomain": name, "record_type": rtype, "value": value,
}).json()
inner = next(iter(reply.values()))
if str(inner.get("ResponseCode")) != "0":
raise RuntimeError(inner.get("Error"))
return bool(inner["Records"]) and all(r["Live"] == "yes" for r in inner["Records"])
def wait_live(host, name, rtype, value, timeout=900, every=15):
deadline = time.time() + timeout
while time.time() < deadline:
if is_live(host, name, rtype, value):
return True
time.sleep(every)
return False
if wait_live("advisor.example.com", "@", "txt",
"google-site-verification=3J9tq0iV8pTt1yW5zKc2mE7aR4bN6hD0sF1gL8xQ"):
print("live: verify it now")
else:
print("not live after 15 minutes: check the record with get_dns")If a record is not live after 15 minutes, something else is wrong: read the host back with get_dns and compare the value character by character (a + that arrived as a space is the usual cause).
Check it yourself#
get_ns returns the authoritative nameservers. Ask one of them directly with dig:
dig +short TXT advisor.example.com @ns1.example.net
dig +short TXT _dmarc.advisor.example.com @ns1.example.netWithout the @server part, dig asks your own resolver, which shows what the public sees (and may still have an old answer cached).