The Sender Policy Framework (SPF) specification (RFC 7208) states that email receivers should limit SPF evaluation to a maximum of 10 DNS lookups. This includes both direct queries for SPF records and any recursive lookups triggered by include, a, mx, ptr, or exists mechanisms.
Each nested SPF record inclusion adds to the total count. Consider this example:
example.com. IN TXT "v=spf1 include:_spf.google.com include:mailgun.org include:servers.mcsv.net ~all"
Now let's examine what happens:
- Initial lookup for example.com's SPF record = 1 lookup
- _spf.google.com's SPF might contain 4 includes = +4 lookups
- mailgun.org might have 3 includes = +3 lookups
- servers.mcsv.net might have 2 includes = +2 lookups
Total: 1+4+3+2 = 10 lookups (right at the limit)
Based on empirical testing and industry reports:
| Provider | Strict Enforcement | Typical Action |
|---|---|---|
| Gmail | No | May mark as spam but rarely rejects |
| Microsoft 365 | Sometimes | May soft-fail messages |
| Yahoo | Yes | Often rejects with "too many DNS lookups" |
| Apple iCloud | No | Treats as neutral |
You can check your lookup count with these tools:
# Using dig and manual count
dig TXT example.com +short | grep "v=spf1"
# Using specialized tools
nslookup -q=TXT example.com
For automated checking, use Python:
import dns.resolver
def count_spf_lookups(domain, current_count=0, max_depth=3):
if current_count >= 10 or max_depth <= 0:
return current_count
try:
answers = dns.resolver.resolve(domain, 'TXT')
for rdata in answers:
for string in rdata.strings:
if string.decode().startswith('v=spf1'):
mechanisms = string.decode().split()[1:]
for mech in mechanisms:
if mech.startswith('include:'):
included = mech.split(':')[1]
current_count += 1
current_count = count_spf_lookups(
included, current_count, max_depth-1)
except:
pass
return current_count
When you exceed the limit:
- Flatten your SPF: Replace includes with direct IP ranges
- Use SPF macros: For dynamic environments
- Prioritize critical services: Remove less important includes
Example of flattened SPF:
v=spf1 ip4:192.0.2.0/24 ip4:198.51.100.123 ip6:2001:db8::/32 -all
Regularly check your SPF record's effectiveness:
# Using SPF validation tools
spfquery --scope mfrom --identity user@example.com --ip 192.0.2.1
Consider setting up automated monitoring with tools like:
- SPF Surveyor
- MXToolbox SPF checker
- Custom scripts with cron jobs
The SPF specification (RFC 4408) states in section 10.1 that receivers should limit the number of DNS queries during SPF evaluation to 10. This includes:
- Direct SPF record lookups - "include:" mechanism resolutions - "a:"/"mx:" record lookups when specified - "ptr:" lookups (though deprecated) - "exists:" macro expansions
Your calculation is correct. For example:
example.com SPF record: v=spf1 include:_spf.google.com include:mailgun.org include:sendgrid.net -all If each included domain has: _spf.google.com → 4 includes mailgun.org → 3 includes sendgrid.net → 2 includes Total lookups: 1 (initial) + 3 + 4 + 3 + 2 = 13 (over limit)
Based on empirical testing and community reports:
| Provider | Enforcement Behavior | |----------------|-------------------------------------| | Gmail | Rejects at 11+ lookups | | Office 365 | Allows up to 20 | | Yahoo | Rejects at 11 | | FastMail | Hard limit at 10 | | Mimecast | Configurable threshold |
To stay under the limit while using multiple ESPs:
// Option 1: Flatten your SPF record
$ dig TXT example.com +short
"v=spf1 ip4:192.0.2.0/24 ip4:198.51.100.123 ip6:2001:db8::/64 -all"
// Option 2: Use SPF macros (advanced)
"v=spf1 include:%{i}._ip.%{o}._spf.%{d} -all"
// Option 3: DNS record consolidation
; Create a dedicated subdomain
_senders.example.com. IN TXT "v=spf1 include:esp1.com include:esp2.com -all"
main.example.com. IN TXT "v=spf1 include:_senders.example.com -all"
Use these tools to audit your SPF chain:
# Using dig manually
dig TXT example.com +short | grep -i "v=spf1"
# SPF validator tools
nslookup -q=TXT example.com
https://mxtoolbox.com/spf.aspx
# Python SPF checker example
import spf
checker = spf.check2(i="sender@example.com",
s="mailfrom",
h="receiving-server.com")
print(checker[0]) # Returns pass/fail and lookup count
For complex enterprise setups:
- Consider moving some services to separate subdomains
- Implement DMARC with p=none to monitor impact
- Contact receiving providers for whitelisting exceptions
The most reliable approach remains keeping your DNS query count at 10 or below for universal acceptance.