SPF Permerror: How the 10-Lookup Limit Works
An SPF evaluation gets a budget of 10 DNS-triggering terms. The count includes include, a, mx, ptr, exists, and redirect across the full evaluation path. The eleventh evaluated term produces permerror.
The awkward part is what “10 lookups” means. It is not the number of include: entries in your top-level TXT record. It is not a count of every DNS packet either. Nested includes share one global budget, while ip4, ip6, and all spend none of it. My rule is simple: count the evaluated path, not the root line.
Here is a complete trace built from hypothetical .test records.
Walk the budget through a nested SPF record
example.test
v=spf1 include:_spf.mail.example.test include:_spf.crm.example.test a mx -all
[1] [9] [ ] [ ]
_spf.mail.example.test
v=spf1 include:_spf-a.mail.example.test include:_spf-b.mail.example.test -all
[2] [6]
_spf-a.mail.example.test
v=spf1 ip4:192.0.2.0/24 include:_spf-c.mail.example.test -all
[0] [3]
_spf-c.mail.example.test
v=spf1 a mx -all
[4] [5]
_spf-b.mail.example.test
v=spf1 include:_spf-d.mail.example.test -all
[7]
_spf-d.mail.example.test
v=spf1 exists:%{i}.authorized.example.test -all
[8]
_spf.crm.example.test
v=spf1 include:_spf-a.crm.example.test -all
[10]
_spf-a.crm.example.test
v=spf1 a -all
[11] → permerror
Assume the connecting IP matches none of the earlier mechanisms. Evaluation starts at the first include, enters its record, and keeps one count across every nested call. The first include costs term 1. Its child includes and their a, mx, or exists mechanisms raise the shared total to 8.
After the mail branch returns no match, the evaluator reaches the CRM include in the root record. Entering it spends term 9. The nested CRM include spends term 10. Its child a mechanism becomes term 11, so evaluation ends with permerror.
Look back at the root TXT record. It shows only two includes, one a, and one mx. A quick top-level count says four. The evaluated path says eleven. This is why counting visible includes misses the failure.
Which SPF terms spend the budget?
RFC 7208, section 4.6.4 defines the limit. Six term types spend one unit when evaluated:
includeamxptrexistsredirect
Four familiar terms do not spend this 10-term budget: all, ip4, ip6, and exp. The exp modifier performs a later lookup only when producing an explanation, so the standard excludes it from this limit.
The ptr mechanism still counts, though the same RFC advises publishers not to use it. Its reverse lookups are slow and less reliable than the alternatives.
Why “10 lookups” is shorthand, not packet accounting
One counted term does not always equal one DNS query. An mx mechanism starts with an MX lookup, then resolves address records for returned exchangers. RFC 7208 places a separate ceiling of 10 A or AAAA records on each evaluated mx mechanism.
The standard also recommends a separate limit of two void lookups. A void lookup returns NXDOMAIN or a successful DNS response with no answer. The third void result is enough for permerror in implementations following the recommended default, even when the main term count stays below 10.
So there are three different numbers worth keeping apart:
- The global count of evaluated DNS-triggering SPF terms, limited to 10.
- The underlying DNS queries produced by terms such as
mx. - Void results, with a recommended limit of two.
Does a record with more than 10 possible terms always fail?
No. SPF evaluates mechanisms from left to right and stops after a match. A sender matching the first include in the example never reaches the CRM branch. A different sender might traverse every non-matching branch and hit term 11.
This is the strongest fair objection to a static SPF counter: expanding every possible branch gives a useful risk audit, but it does not reproduce every sender-specific evaluation. A proper test needs three inputs:
- the SMTP client IP;
- the
MAIL FROMor HELO identity under evaluation; - live DNS records at evaluation time.
Microsoft’s current SPF guidance makes the same distinction: evaluation stops once a source matches, so one sender path might pass while another path reaches the limit.
How do you confirm lookup-limit permerror?
Start with the message evidence. Open the raw headers and find the trusted receiver’s Authentication-Results field. A relevant result often resembles this hypothetical line:
Authentication-Results: mx.receiver.test;
spf=permerror reason="too many DNS lookups"
smtp.mailfrom=alerts.example.test;
permerror means the published SPF policy needs operator work. It differs from temperror, which reports a transient problem such as a DNS timeout. A retry might clear temperror; it does not repair an 11-term SPF path.
Only trust an Authentication-Results field inserted by your own receiver or another trusted boundary. Headers copied from an untrusted incoming message are forgeable. RFC 8601 defines this header and its trust-boundary rules.
Next, open the site’s free SPF checker for the root sending domain. It shows the published record, terminal policy, includes, and redirect target. The tool deliberately does not claim a sender-specific recursive verdict, so follow each referenced record and write down the cumulative term count for the failing path.
Fix the record without losing an authorized sender
The safest fix begins with inventory, not flattening. List every system sending with the affected MAIL FROM domain, identify its documented SPF requirement, and remove references for retired services. An unused include costs lookup budget and keeps an old sender authorized.
Next, replace DNS-dependent mechanisms only where stable addresses and operational ownership justify it. RFC 7208 notes the tradeoff: ip4 and ip6 cost no lookup terms, while a and mx follow DNS changes at evaluation time. Hard-coded ranges reduce lookups but require prompt maintenance when infrastructure changes.
For unrelated sending systems, move mail onto dedicated subdomains. Transactional mail might use notify.example.test, while campaigns use news.example.test. Each sending identity gets its own SPF policy and budget. This also separates operational changes and reputation. Confirm the service supports the chosen envelope-sender domain before moving it.
Automated SPF flattening deserves caution. A flattened record turns provider-managed DNS references into copied IP ranges. It lowers lookup use today but transfers change tracking to you. If a provider changes its ranges and the flattened record stays stale, legitimate mail fails SPF. Keep the original source mapping, refresh before expiry, monitor changes, and retain DKIM plus DMARC rather than treating SPF as the only authentication layer.
The SPF, DKIM, and DMARC alignment guide shows why an SPF pass by itself does not guarantee a DMARC pass. Solving permerror repairs one authentication input; it does not prove message safety or inbox placement.
Publish, then test each real sending path
After updating DNS, wait for the old TTL to expire and send through every authorized service. Inspect the receiver’s raw headers for the envelope sender in question. Record SPF result, evaluated domain, DKIM result, and DMARC result separately.
I would keep the term budget below the edge rather than celebrating a count of exactly 10. Third-party includes change outside your deployment cycle. Spare room turns a provider’s nested-record update into an alert instead of a production mail failure.