MTA-STS max_age and the policy cache: why mail bounced a week after you changed MX.

The MTA-STS policy you publish is not the policy senders apply. They apply the copy they fetched last time, for as long as max_age says, and the id in your DNS record is the only thing that makes them look again. Most MTA-STS incidents are this one fact, met at the wrong moment.

Written by · Published 2026-09-24 · 6 min read

What a sender does with your policy

RFC 8461 section 3.3 describes the sender's side, and it is worth reading once because the whole caching problem is in it. Every time a sending server has a message for your domain it does this:

  1. Looks up the TXT record at _mta-sts.yourdomain and reads the id.
  2. If it has a cached policy for your domain, the cached id matches, and the cache has not passed its max_age, it uses the cached policy. No HTTPS fetch.
  3. If the id has changed, or the cache has expired, or there is no cache, it fetches https://mta-sts.yourdomain/.well-known/mta-sts.txt, validates it, and caches the result for max_age seconds.
  4. If that fetch fails for any reason (host down, certificate expired, redirect, wrong content), it keeps using whatever it had cached until that copy expires. If it had nothing cached, it delivers as though you had no policy.

So there are two clocks. The id is the fast one: change it and senders re-read your policy on their next delivery. max_age is the slow one: it is how long a copy survives when nobody tells the sender to look again, or when the sender tries to look and cannot.

The two ways this goes wrong

1. You changed the policy and did not change the id

Senders keep applying the old policy for up to max_age. If the old policy is mode: enforce and lists MX hosts you no longer use, every enforcing sender (Gmail, Outlook.com, and a growing list of others) refuses to deliver to your new hosts, retries for a few days, and then bounces. The bounce reason usually names MTA-STS, but it is on the sender's side, so your users see "message could not be delivered" from their correspondents, and you see nothing at all in your own logs.

2. The policy host stopped answering

The certificate on mta-sts.yourdomain expired, someone put a redirect in front of it, the static host it lived on was decommissioned. Senders that have a cached copy carry on with it until max_age runs out. Then they treat you as having no policy, and the protection you set MTA-STS up for is gone. Nothing bounces, nothing is logged, and unless you are reading TLS-RPT reports, which would show "policy could not be fetched" for weeks, you find out when someone asks why your domain no longer scores on a security scan.

The MX migration, step by step

Here is the sequence that produces the bounce, so you can see where the cache bites.

WhenWhat you didWhat an enforcing sender does
BeforePolicy: mode: enforce, mx: mx1.oldhost.example, max_age: 1209600 (two weeks)Cached that policy at its last fetch, up to two weeks ago
Day 0Change MX records to the new providerLooks up MX, gets the new hosts; checks its cached policy; no mx line matches them; refuses to connect
Day 0 to 2Nothing, because nothing looks wrong on your sideRetries on its normal schedule, fails the same way each time
Day 2 to 5Users report bounces from Gmail contactsGives up and bounces the message to its sender
Day 5Update the policy file with the new hostsStill using the cached copy: the id did not change
Day 5 + a minuteChange the id in the TXT recordNext delivery attempt sees the new id, fetches the new policy, delivers

The order that does not lose mail is the reverse. Add the new MX hosts to the policy alongside the old ones, change the id, and wait at least max_age so that every sender's cache has had a chance to refresh (they refresh on their next delivery to you, so a busy sender refreshes within minutes and a quiet one may take the full period). Then change the MX records. Then, once the old hosts are out of service, remove them from the policy with another new id. During the transition both sets of hosts need valid certificates, because the policy says they are all legitimate.

Choosing max_age

The RFC allows up to 31557600 seconds (a year) and its own example uses 604800 (a week), which is where most deployments end up. The trade-off is simple to state:

max_ageProtection if the policy host is downRecovery if you publish a mistake and forget the id
86400 (a day)Lapses within a dayWithin a day
604800 (a week)Holds for a weekUp to a week of refused mail
2592000 (thirty days)Holds for a monthUp to a month

A day in testing mode, a week or two in enforce, is the pattern that has served well. Longer only makes sense if the policy host is on infrastructure you trust more than your mail servers, and it always means planning MX changes around the value. Shorter than a day in enforce mode is asking for the protection to lapse at the first hiccup on the policy host.

Checking where you stand

The email deliverability checker fetches the policy the way a sender does (no redirects followed), parses every line, matches each mx pattern against your real MX hosts, and writes max_age out as a duration with the sentence that matters: a sender that holds a copy keeps applying it for that long unless the id changes. An MX host the policy does not cover in enforce mode is graded as a fail, because that is a bounce in progress, not a warning. In testing mode the same finding is what to fix before you switch.

From a terminal, the two things to look at are the record and the raw fetch:

dig +short TXT _mta-sts.example.com
curl -sSi https://mta-sts.example.com/.well-known/mta-sts.txt

You want a 200, Content-Type: text/plain, no Location header, and a body whose mx lines you can match by eye against dig +short MX example.com. If you are about to change something, note the id you are replacing, because the most common fix for "I changed the policy and nothing happened" is realising you did not change it.

Retiring a policy

The same cache applies in reverse. Deleting the TXT record and the file leaves senders with a cached enforce policy until it expires; if you also changed hosts, you have the migration bounce with no way to publish a fix. Publish mode: none with a new id first, leave it in place for at least the old max_age, and only then remove the record and the file.

All guides · All tools