A mail server that rejects your mail with no explanation and no bounce is usually not broken. It evaluated your SPF record, hit a hard limit, got permerror, and treated the domain as unauthenticated. The limit is explicit in the specification, and most SPF records in the wild that "suddenly stopped working" crossed it by adding one more include.
The number, and which terms count
RFC 7208 section 4.6.4, "DNS Lookup Limits," is unambiguous. Six terms cause DNS queries at evaluation time:
includeamxptrexistsredirect
Implementations MUST limit the total number of those terms to 10 during evaluation. Exceed it and the implementation MUST return permerror. The terms all, ip4, ip6 and the exp modifier do not count, because they resolve nothing.
Two details make this worse than it looks. First, the limit is global across the whole evaluation, not per nested include — the RFC says the limit "must be tracked as a single global limit for all evaluations." So an include that itself contains include chains eats the same budget. Second, the mx mechanism counts MX record queries toward the same 10, so a domain with many MX hosts burns the budget without you writing a single extra include.
Why it presents as "no SPF"
Receivers treat permerror as a permanent error and, in practice, as an absent policy. A message with no usable SPF still usually gets delivered when DMARC is in none mode — so the failure is invisible until DMARC moves to quarantine or reject, at which point everything from that domain starts landing in spam or bouncing. The gap between the trigger and the symptom is often months, which is why the cause gets misattributed.
There is a second trap in the same section: void lookups. A void lookup is a DNS query that returns no answer. The RFC says implementations SHOULD limit void lookups to two. So even under the 10-term ceiling, a couple of includes pointing at domains that no longer publish records can independently sink evaluation.
How to count before you break it
Do not count by eye. Every serious SPF tooling does the walk for you and reports nested lookups, and the number it prints is the one that matters. When you audit a record, the questions are:
- How many
includeterms are there at the top level? - How many does each of those resolve internally, recursively?
- Does any
mxterm pull a large MX set into the budget? - Does the record use
ptrat all? Almost nothing should — RFC 7208 section 5.5 formally deprecates it.
The fix is subtraction
Three moves, in order of impact:
- Drop
ptr. Deprecated, slow, and it counts against the budget. - Flatten. Replace an
includethat only exists to add a handful of IPs with explicitip4:/ip6:terms. Those cost zero lookups. The trade-off is maintenance: flattened records stop tracking the provider's IP changes, so flatten only stable ranges. - Consolidate senders. Every marketing platform, CRM, and ticketing system wants its own include. Each one is a permanent tax on the same 10-term budget. A record with seven vendor includes has 3 lookups left for everything else, forever.
What to do when it is already broken
If mail is currently failing, do not start by rewriting all. Change the record so the count drops below the ceiling while keeping the existing pass/fail behaviour for legitimate senders, verify with the tooling, then tighten policy. Reordering mechanisms so cheap ones match first also helps: SPF is evaluated left to right and stops at the first match, so ip4 terms placed early resolve the common case without spending lookups.
The takeaway
10 DNS-lookup terms, tracked globally, with mx included and void lookups capped at two. Cross it and SPF reports permerror, which reads as "no policy" and usually fails silently until DMARC enforcement arrives. Count before adding, flatten what is stable, and treat every new vendor include as a permanent subtraction from a fixed budget.