SPF DNS lookup limit (10) and how to trace a permerror
SPF turned into permerror after adding an include, or fails only for some senders: how RFC 7208 counts DNS-lookup terms, how to count them by hand, and how to reduce them.
Sources and commands checked:
You added one include: to your SPF record and receivers now report spf=permerror, or SPF fails only for mail from some senders. The first thing to check is the limit RFC 7208 puts on DNS lookups.
The rule (RFC 7208 §4.6.4)
- The terms that query DNS during evaluation are the
include,a,mx,ptr, andexistsmechanisms and theredirectmodifier. SPF implementations MUST limit the total of those terms to 10 and returnpermerrorwhen the limit is exceeded. all,ip4,ip6, andexpdo not query DNS during evaluation and are not counted.expcan still cause a TXT query for its domain later, to fetch the explanation after the result isfail(RFC 7208 §4.6.4, §6.2); that query does not count toward the limit of 10.- The count covers the whole evaluation: terms in the records reached through
include:andredirect=add to the same total. - In addition, evaluating one
mxmust not query more than 10 address records (A or AAAA); beyond that, thatmxproducespermerror. - A lookup that returns an empty answer (NOERROR with no records) or a nonexistent name (NXDOMAIN) is a "void lookup", and implementations SHOULD limit them to two. A leftover
include:for a service you no longer use can trigger this.
Example: counting
Consider this setup. The domains and addresses are reserved for documentation (domains from RFC 2606, IPv4 from RFC 5737, IPv6 from RFC 3849). The TXT content on each line can be pasted into SPF Record Parser as it is.
example.com. TXT "v=spf1 mx include:_spf.mail.example.net include:spf.crm.example.org a:relay.example.com -all"
_spf.mail.example.net. TXT "v=spf1 include:_nb1.mail.example.net include:_nb2.mail.example.net include:_nb3.mail.example.net ~all"
_nb1.mail.example.net. TXT "v=spf1 ip4:192.0.2.0/26 ip6:2001:db8:1::/48 ~all"
_nb2.mail.example.net. TXT "v=spf1 ip4:192.0.2.64/26 ip6:2001:db8:2::/48 ~all"
_nb3.mail.example.net. TXT "v=spf1 ip4:192.0.2.128/26 ip6:2001:db8:3::/48 ~all"
spf.crm.example.org. TXT "v=spf1 include:spf1.crm.example.org include:spf2.crm.example.org ip4:198.51.100.0/24 ~all"
spf1.crm.example.org. TXT "v=spf1 ip4:203.0.113.0/25 ~all"
spf2.crm.example.org. TXT "v=spf1 ip4:203.0.113.128/25 ~all"| Record | Terms that query DNS | Count |
|---|---|---|
example.com | mx, include ×2, a | 4 |
_spf.mail.example.net | include ×3 | 3 |
spf.crm.example.org | include ×2 | 2 |
_nb1–_nb3.mail.example.net, spf1 / spf2.crm.example.org | none (only ip4 / ip6) | 0 |
Total for a sender that matches nothing, so that evaluation reaches the final -all | 9 | |
The 9 is the number of limited terms reached in evaluation order. Evaluation stops as soon as the sender's address matches, so a sender that matches earlier counts fewer terms (mail sent from the _nb1 range that does not match mx, for example, passes after three: mx, include:_spf.mail.example.net, and include:_nb1.mail.example.net). It is also not the number of DNS queries itself (mx is followed by address queries for each MX).
Adding one more include: for a service whose record has two include: terms of its own makes the total 9 + 1 + 2 = 12, over the limit.
Why only some senders fail
Mechanisms are evaluated left to right, and evaluation stops at the first match (RFC 7208 §4.6.2). A sender that matches the early mx or the first include: passes before the count reaches 10; only senders matched by a later include:, or not matched at all, hit the limit and get permerror. That can explain "the record works, but mail sent through one service fails".
Counting by hand
- Fetch your domain's TXT records and find the one that starts with
v=spf1. Two or morev=spf1records are apermerrorby themselves (RFC 7208 §4.5).dig +short TXT example.com - Paste the record into SPF Record Parser to see how many DNS-querying terms that record contains. A TXT record split into several strings can be pasted as it is.
- Repeat steps 1 and 2 for each
include:andredirect=target and add the counts. A domain reached twice is counted twice. - Count empty or NXDOMAIN answers separately as void lookups.
QuickKit does not query DNS, so SPF Record Parser counts only the one record you paste. The total for an evaluation is the sum over every record it reaches.
Reducing the count
- Remove
include:terms for services you no longer use; they can also cause void lookups. ptrSHOULD NOT be published (RFC 7208 §5.5); consider removing it.- For addresses you control that do not change, replacing
aormxwithip4:/ip6:lowers the count. - Expanding a service's
include:into its IP addresses ("flattening") breaks silently when the service changes its addresses, and legitimate mail starts failing. If you do it, pair it with something that tracks those changes. - SPF checks mainly the domain of the SMTP MAIL FROM (envelope sender) (RFC 7208 §2.4). If each service uses its own subdomain in MAIL FROM, that service's
include:can move to that subdomain's record. If you use DMARC, check that alignment still holds afterwards.
Limits and caveats
- Whether the limit is hit depends on the sender's address and the evaluation order. Keeping the worst case (a sender that matches nothing) at 10 or fewer is the safe target.
- Slow or failing DNS answers can produce
temperror, which is a different problem from the lookup count. - This guide explains the RFC 7208 rules. Check the receiving system for its actual verdict and how it reports it.