spf=permerror instead of spf=pass. None of the old services changed, which makes this confusing. The edit that broke everything looks small and harmless.
This article treats the problem as a post-mortem. It covers the impact, the order an investigation usually takes, where the real cause turns up, what to do in the first hour, and what to change so it does not happen again.
Initial impact: why one edit hits every sender
p=reject.
The receiving server's Authentication-Results header is the fastest confirmation:
TEXT
# Illustrative output format (domains are placeholders)
Authentication-Results: mx.receiver.example;
spf=permerror (too many DNS lookups) smtp.mailfrom=example.com;
dkim=pass header.d=example.com;
dmarc=pass header.from=example.comInvestigation: where people usually look first
The discovery: lookup number eleven
include, a, mx, ptr, exists, and the redirect modifier. The terms ip4, ip6, and all cost nothing because the receiver does not need DNS to check them. Going over 10 is a permerror, not a soft warning.
Nested includes are where people lose count. An include points to another provider's SPF record, and that record often includes several more of its own. Each of those counts toward your 10. Here is how a record that looks like it has five or six terms can actually use eleven:
TEXT
# Illustrative example: lookup count for one domain (placeholder names)
v=spf1 a mx include:_spf.service-a.example include:spf.service-b.example include:mail.crm.example include:spf.newtool.example -all
a 1
mx 2
include:_spf.service-a.example 3
-> includes 2 more records 4, 5
include:spf.service-b.example 6
-> includes 2 more records 7, 8
include:mail.crm.example 9
-> includes 1 more record 10
include:spf.newtool.example 11 -> permerrorv=spf1. Two such records is a permerror, even if each is valid on its own. This often happens when a control panel creates a default SPF record and someone adds a second one for the new service instead of editing the first.
The second is void lookups. A void lookup is a query that returns no answer or NXDOMAIN. The RFC recommends allowing no more than 2 of these. An include left behind for a cancelled service whose record was deleted can trigger this.
You can check all three with dig, which is read-only:
BASH
# List every TXT record at the root.
# More than one line starting with v=spf1 is a permerror by itself.
dig +short TXT example.com
# Follow each include one level down, then repeat for anything it includes.
# Count every include, a, mx, ptr, exists, and redirect along the way.
dig +short TXT _spf.service-a.exampledig on an include returns nothing, it is a void lookup and probably a leftover worth removing.
Temporary mitigation: getting back under ten
- If there are two
v=spf1records, merge them into one and delete the other. This alone fixes some cases. - Remove includes for services the domain no longer uses. Old marketing tools and abandoned helpdesks are common leftovers.
- Replace
aandmxwithip4:orip6:entries when they resolve to your own server's fixed addresses. Each replacement saves a lookup. Only do this for addresses you control and know will not change. - Remove
ptrentirely. The RFC discourages it, and it costs lookups. - If you are still over, take the new include out for now. Check whether that service signs with aligned DKIM, because if it does, its mail can pass DMARC without being in SPF while you plan the real fix.
The architectural fix: stop putting every sender on the root domain
ip4 and ip6 ranges and publish those directly, for example v=spf1 ip4:192.0.2.10 ip4:198.51.100.0/24 ip6:2001:db8::/32 -all. That uses zero lookups. The trade-off is that you now own data that belongs to your providers. When a provider adds or changes sending ranges, your flattened record goes stale without any warning. Mail from the new ranges then gets a plain SPF fail rather than permerror, which is harder to spot. If you flatten, treat it as something that needs automation and monitoring, not a one-time edit. Also watch the record length: a single TXT string tops out at 255 characters, and long records must be split into multiple strings.
The more durable fix is to give each third-party sender its own envelope sender domain, such as bounce.example.com or mail.example.com. Many email services support a custom return-path or bounce domain for this purpose. Check each service's documentation for the exact records it needs. That subdomain gets its own SPF record with its own budget of 10 lookups, and the root record only lists what actually sends with the root domain as the envelope sender. DMARC alignment is relaxed by default, so a subdomain of your organizational domain still aligns, unless your DMARC record sets aspf=s.
Pair this with aligned DKIM on every service. SPF breaks for reasons that have nothing to do with you, such as forwarding, provider IP changes, or someone else's nested include growing. DKIM survives most of those, so DMARC has a second path to pass.
In practice, the habit that prevents a repeat is simple. Before adding any include, count the lookups the record already uses and ask whether the new service even needs to be in the root record. If the count is already at eight or nine, the next edit should be moving a sender to a subdomain, not adding one more include.