The clock that should drive your DNS TTL is not propagation speed. It's evidence. When you move company mail to a new provider, the proof that the new path actually delivers arrives on a reporting cadence, and DMARC's requested report interval defaults to 86400 seconds. So pick a 300-second TTL for the change window, put it in place one full old-TTL period before the planned cutover, and leave the long TTL — 3600 seconds or more — in place for every other day of the year.
Propagation is the easy part.
Picture a newsroom domain. tips@, press@, an editorial alias that half the PR firms in the city have had on file since forever, plus the ad-ops mailbox that invoices land in. Losing an hour of that inbound mail is not an abstract SLO breach — it's a spiked story and an unpaid invoice. That's the system this article is about, and it's why TTL selection here gets judged on one axis: how fast can you prove the new path works, and how fast can you get back if it doesn't?
The resolver cache map, before and after
Draw it as three boxes. On the left, thousands of sending mail servers, each with its own resolver. In the middle, those resolvers, each holding your MX RRset with an independent countdown that started whenever that resolver first asked. On the right, your authoritative nameservers, which are the only box you actually control.
Before pre-lowering, the middle box is opaque and slow: a 3600-second TTL means a resolver that asked 10 seconds ago will keep routing mail to the old provider for another 59 minutes, no matter what you publish now. After pre-lowering, the middle box drains fast and refills fast. Same topology, different time constant.
That's the whole trick. You are not speeding up DNS. You are shrinking the window during which your published intent and the answers senders receive can disagree.
How short should a DNS TTL be before a planned mail cutover?
Short TTLs are a temporary instrument, not a posture. The value that matters is the TTL the record carried before you touched anything, because that's the one still ticking inside caches you can't see. Lower the MX RRset to 300 seconds at least one full old-TTL period ahead — with a 3600-second steady state, that's a T-minus-one-hour action; with a 86400-second steady state, it's a T-minus-one-day action, and that difference is the single most common planning miss I see people make.
Then hold the low value through the change and through the evidence window, not just through the change.
| Phase | MX TTL | What you watch |
|---|---|---|
| Steady state | 3600–86400 s | an alert on unexpected MX answers |
| Pre-lowering, T-minus one old TTL | 300 s | old TTL draining across resolvers |
| Cutover | 300 s | the MX answer set, per resolver |
| Evidence window, roughly one report interval | 300 s | DMARC aggregate report rows |
| Restore | 3600 s or more | back to the alert |
Why not 60 seconds? Because a TTL floor that low multiplies authoritative query load and trades away your only buffer against an authoritative outage, and caching is what buys you that stability. RFC 8767 lets a resolver keep serving expired answers when your authoritative servers are unreachable, which is a genuine safety net — but it's a resolver-side choice, not a guarantee you can plan around. Three hundred seconds is short enough that a bad cutover costs you five minutes of misrouted mail, and long enough that your zone isn't answering the entire internet every minute.
Two mechanical details bite people here. First, the TTL is an upper bound on caching, not a promise: RFC 2181 pins the value as an unsigned 32-bit number with a maximum of 2147483647, and explicitly frames it as the longest a record may be cached, so a resolver is free to discard it sooner. Second, negative answers have their own cache. If you publish the new MX hostname before its A record exists, resolvers cache the failure for the SOA MINIMUM period — RFC 2308 recommends 1 to 3 hours for that field — and lowering the MX TTL does nothing to flush it. Publish the target host's address records first. Always.
One more rule that has nothing to do with TTL and breaks cutovers anyway: an MX must point at a hostname with address records, never at an alias. RFC 2181 says so plainly, and RFC 5321 describes the resolution path that depends on it.
A copyable watcher for MX answers and TTL countdowns
Observability for a cutover is not a status page. It's a loop that asks several independent resolvers the same question and writes down what they said. Here's the whole thing, in TypeScript, no dependencies:
import { Resolver } from "node:dns/promises";
const DOMAIN = "newsroom.example";
const TARGET = ["mx1.newmail.example", "mx2.newmail.example"];
const RESOLVERS = (process.env.MX_WATCH_RESOLVERS ?? "9.9.9.9,8.8.8.8,1.1.1.1").split(",");
type Probe = {
at: string;
resolver: string;
exchanges: string[];
topExchangeTtl: number | null;
onTarget: boolean;
};
async function probe(server: string): Promise<Probe> {
const r = new Resolver({ timeout: 3000, tries: 2 });
r.setServers([server]);
const mx = await r.resolveMx(DOMAIN);
const exchanges = mx
.sort((x, y) => x.priority - y.priority)
.map((m) => m.exchange.toLowerCase().replace(/\.$/, ""));
// resolveMx does not expose the MX RRset TTL, so read the countdown
// from the A record of the most-preferred exchange instead.
let topExchangeTtl: number | null = null;
if (exchanges[0]) {
const a = await r.resolve4(exchanges[0], { ttl: true });
topExchangeTtl = a[0]?.ttl ?? null;
}
return {
at: new Date().toISOString(),
resolver: server,
exchanges,
topExchangeTtl,
onTarget: exchanges.join(",") === TARGET.join(","),
};
}
for (const server of RESOLVERS) {
try {
console.log(JSON.stringify(await probe(server)));
} catch (err) {
console.log(JSON.stringify({ at: new Date().toISOString(), resolver: server, error: String(err) }));
}
}
Run it on a 60-second timer from the moment you pre-lower, ship the lines to wherever your logs already go, and graph one number: the share of probed resolvers where onTarget is true. That single ratio is your cutover progress bar, and the error lines are the ones that will save you, because a resolver that times out is not a resolver that agrees with you.
The comment in there is load-bearing. Node's resolveMx gives you exchange and priority but no RRset TTL, so the A-record countdown is a proxy for how fresh that resolver's view is. If you want the MX RRset TTL itself, dig +noall +answer MX newsroom.example prints it in the second column, and it's worth eyeballing once by hand before you trust any dashboard.
Deliverability evidence sets the rollback clock
Here's the part that changes the whole schedule. An MX change is inbound; deliverability evidence is mostly outbound, and it does not arrive in real time.
DMARC aggregate reports are the closest thing you have to a receiver-side ground truth: RFC 7489 defines an XML report summarising, per sending source, how many messages passed SPF, passed DKIM, and aligned with your domain. The ri tag requests the interval between those reports, and its default is 86400 seconds. Receivers are not obliged to honour a shorter one. So if your new provider signs with a new DKIM selector and sends from new IPs, the data that tells you alignment survived the move shows up roughly a day later, in bulk, from a handful of large receivers.
Which means your rollback window is bounded by report latency, not by TTL. Keep the old provider's mailboxes reachable and the low TTL published until at least one report cycle has landed and you've read it.
const HOUR = 3600, DAY = 86400;
function cutoverPlan(t0: number, oldTtl = HOUR, changeTtl = 300, reportInterval = DAY) {
const iso = (s: number) => new Date(s * 1000).toISOString();
return {
preLowerAt: iso(t0),
cutoverNotBefore: iso(t0 + oldTtl), // old TTL has drained
readEvidenceAfter: iso(t0 + oldTtl + reportInterval), // first aggregate report
retireOldPathNotBefore: iso(t0 + oldTtl + reportInterval + changeTtl),
};
}
There's a second cache most runbooks forget, and it ignores DNS TTL entirely. If the domain publishes an MTA-STS policy, senders fetch it over HTTPS and cache it for up to max_age, which RFC 8461 caps at 31557600 seconds. A sender holding a policy that lists only your old provider's MX hostnames will refuse to deliver to the new ones — TLS-secured refusal, not a fallback. Bump the id value in the _mta-sts TXT record so senders refetch, and make sure the new policy lists both old and new MX hosts during the overlap. This is the failure mode I'd put money on for any domain with a mature mail posture.
The catch is that all of this assumes you can schedule the change. Pre-lowering isn't a good fit for an emergency migration off a provider that's already gone, where you're choosing between misrouted mail and no mail at all; there you publish immediately and accept that the old TTL rules the recovery. It also doesn't help if your zone is hosted somewhere that doesn't support per-record TTL overrides — stick with a hard maintenance window and an explicit customer notice in that case, because a runbook that assumes control you don't have is worse than no runbook.
Two objections worth answering
"Why not just leave everything at 60 seconds?" Because you'd be paying an availability tax all year for a capability you need twice. Long TTLs are what keep mail flowing when your DNS provider has a bad afternoon, and the trade-off only makes sense during a change window. Treat a short TTL like a feature flag: on for the migration, off afterwards, and alert if it's still on a week later.
"Isn't a propagation checker enough?" It samples resolvers your senders may never use, and it can't see the two caches that actually break mail cutovers — negative caching and MTA-STS policy caching. More to the point, it answers the wrong question. It tells you records changed; you need to know whether mail arrived and authenticated. Only report data answers that, and I'm honestly not sure any external checker ever will.
So the decision rule for the newsroom, in one line: if the change is planned, pre-lower one old-TTL period ahead, cut over, hold at 300 seconds until an aggregate report confirms alignment, then restore the long TTL and keep the MX alert. If the change isn't planned, skip straight to publishing and spend your attention on the MTA-STS policy instead.
Boring cutovers are a scheduling achievement, not a DNS one.
Sources
- RFC 7489, Domain-based Message Authentication, Reporting, and Conformance (DMARC): https://datatracker.ietf.org/doc/html/rfc7489
- RFC 1035, Domain Names — Implementation and Specification: https://datatracker.ietf.org/doc/html/rfc1035
- RFC 2181, Clarifications to the DNS Specification: https://datatracker.ietf.org/doc/html/rfc2181
- RFC 2308, Negative Caching of DNS Queries: https://datatracker.ietf.org/doc/html/rfc2308
- RFC 5321, Simple Mail Transfer Protocol: https://datatracker.ietf.org/doc/html/rfc5321
- RFC 8461, SMTP MTA Strict Transport Security (MTA-STS): https://datatracker.ietf.org/doc/html/rfc8461
- RFC 8767, Serving Stale Data to Improve DNS Resiliency: https://datatracker.ietf.org/doc/html/rfc8767
- Node.js DNS module documentation: https://nodejs.org/api/dns.html
Top comments (0)