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.