"SPF too many DNS lookups" means that evaluating your SPF record requires more than the 10 DNS lookups RFC 7208 allows, so receivers stop and return permerror. A permerror means SPF does not pass for any of your mail, not just the sender that tipped you over. To fix it, remove senders you no longer use, replace a and mx with ip4 where you can, move bulk senders to their own subdomains, and flatten only as a last resort.
We use example.com and made-up provider domains like spf.vendor-a.example below. Real providers' includes change over time, so always count against what DNS returns today.
What counts toward the 10-lookup limit
| Term | Counts? | Notes |
|---|---|---|
include: |
Yes | Plus every lookup inside the included record |
a, a:host |
Yes | |
mx, mx:host |
Yes | Counts once; it may resolve at most 10 MX hosts |
ptr |
Yes | Discouraged by RFC 7208; avoid it |
exists: |
Yes | |
redirect= |
Yes | |
ip4:, ip6: |
No | Addresses are written in the record |
all |
No |
The limit is on the whole evaluation, across every level of includes. A record with four includes can easily need 12 lookups if each include contains two more.
RFC 7208 also limits "void lookups", queries that return no records, to two. An include pointing at a domain that no longer publishes SPF burns one of those, and a third gives permerror too. Old includes for services you have cancelled are a common cause.
Why the limit exists
Every SPF check makes the receiving server do DNS work on behalf of a stranger. Without a cap, a hostile record could make receivers fire off huge numbers of queries per message, or use them to flood a third party's DNS. The 10-lookup cap keeps evaluation cheap and predictable. Some receivers may be lenient, but you cannot know which, so assume it is enforced strictly: plan to stay comfortably under 10, ideally with a couple of lookups spare for the next tool someone signs up for.
Step 1: Count your lookups
Start with your record:
dig +short TXT example.com | grep spf1
Say it returns:
"v=spf1 mx include:spf.vendor-a.example include:spf.vendor-b.example include:spf.vendor-c.example include:spf.vendor-d.example ~all"
That is 5 lookups at the top level (mx and four includes). Now query each include:
dig +short TXT spf.vendor-a.example
dig +short TXT spf.vendor-b.example
If spf.vendor-a.example contains three more includes, and each of those is plain ip4 ranges, vendor A costs 4 lookups in total (its own include plus three). Repeat for every include, all the way down. Write it out:
| Term | Lookups |
|---|---|
mx |
1 |
include:spf.vendor-a.example (contains 3 includes) |
4 |
include:spf.vendor-b.example (contains 2 includes) |
3 |
include:spf.vendor-c.example |
1 |
include:spf.vendor-d.example (contains 1 include) |
2 |
| Total | 11: permerror |
To automate the walk, this small bash function follows includes and redirects recursively and prints the total. It is a rough check, not a full SPF evaluator: it does not count void lookups or handle macros.
spfcount() {
local domain=$1 total=0 rec term
rec=$(dig +short TXT "$domain" | grep 'v=spf1' | sed 's/" "//g; s/"//g')
for term in $rec; do
term=${term#[+~?-]}
case "$term" in
include:*) total=$((total + 1 + $(spfcount "${term#include:}"))) ;;
redirect=*) total=$((total + 1 + $(spfcount "${term#redirect=}"))) ;;
a|a:*|a/*|mx|mx:*|mx/*|ptr|ptr:*|exists:*) total=$((total + 1)) ;;
esac
done
echo "$total"
}
spfcount example.com
Our email checker and other SPF checkers do this count for you. Counting by hand once is still worth it, because it shows which sender is expensive.
Step 2: Remove what you don't use
Before any clever fix, check every include against reality:
- Is the service still in use? Cancelled newsletter tools, old CRMs and trial accounts linger in SPF for years.
- Does the service actually send with your domain in the envelope sender? Many providers use their own bounce domain by default. In that case their include on your domain does nothing for SPF and can be removed; DKIM with your domain is what makes their mail pass DMARC.
- Is the same provider included twice under different names?
Look at your DMARC aggregate reports to see which IPs actually send as your domain. Anything not appearing there for a few months is a candidate for removal. Ask whoever owns each tool before deleting its include; a forgotten invoicing system is exactly the kind of sender that breaks quietly.
Step 3: Replace a and mx with ip4
If mx or a refer to your own servers, write the IPs instead:
v=spf1 mx include:spf.vendor-a.example -all
becomes:
v=spf1 ip4:203.0.113.10 include:spf.vendor-a.example -all
That is how our own records work: v=spf1 ip4:<ip> -all on the domain, with no lookups at all. The trade-off is that if the server's IP changes, you must update SPF, but you would be changing DNS anyway.
Also: mx in your SPF is only correct if your inbound MX hosts also send outbound mail. If your MX is a hosted mailbox provider, its include probably already covers it, so mx is wasted.
Step 4: Give bulk senders their own subdomain
This is the fix we recommend for anything long-term. SPF is checked against the envelope sender domain, so a sender that uses news.example.com as its bounce domain is checked against news.example.com's SPF record, not the root's.
| Name | SPF record |
|---|---|
example.com |
v=spf1 ip4:203.0.113.10 include:spf.office.example -all |
news.example.com |
v=spf1 include:spf.vendor-a.example -all |
billing.example.com |
v=spf1 include:spf.vendor-b.example -all |
Each record now has its own 10-lookup budget. Most providers support this under a name like "custom return path", "custom bounce domain" or "sending subdomain". With relaxed DMARC alignment (the default), news.example.com aligns with a From of example.com. If you use aspf=s, it does not, and you rely on DKIM for alignment.
Moving an existing sender to a subdomain takes four steps:
- Publish SPF for the subdomain (
news.example.comabove), with only that provider's include. - In the provider's settings, set the custom return path or bounce domain to
news.example.com, adding any CNAME or MX records it asks for. - Send a test and check
smtp.mailfrom=news.example.comandspf=passin Gmail's "Show original". - Only then remove that provider's include from the root record.
A side benefit: if one stream's reputation suffers, the damage is more contained, and DMARC reports show each stream separately.
Step 5: Flatten, carefully, if you must
Flattening replaces an include with the IP addresses it currently resolves to:
# See what an include expands to today
dig +short TXT spf.vendor-c.example
"v=spf1 ip4:198.51.100.0/24 ip4:192.0.2.0/24 -all"
You then put ip4:198.51.100.0/24 ip4:192.0.2.0/24 in your own record instead of the include, saving the lookup.
The trade-offs:
| Include | Flattened | |
|---|---|---|
| Lookups | 1 or more | 0 |
| Follows provider IP changes | Automatically | No: you must rebuild it |
| Failure mode | Over the limit: permerror for everything | Stale: mail from new provider IPs fails SPF |
| Maintenance | None | Ongoing, ideally automated with alerts |
If you flatten, do it for providers with small, stable IP lists, keep the original includes documented, and use a script or service that rebuilds the record on a schedule and alerts on changes. Never flatten a large cloud provider's include by hand.
Step 6: Watch the record length too
Each TXT string is limited to 255 characters. A longer SPF record is published as several quoted strings in one TXT record, which receivers join together. Most DNS panels do this for you. Keep the whole record reasonably short anyway: very long DNS answers can run into UDP size limits on some resolvers.
Common mistakes
- Adding a second SPF record to hold the extra includes. Two
v=spf1records is its own permerror. One record per name, always. - Assuming the old record was fine because it worked last year. Providers add nested includes over time; your count can go up without you touching anything.
- Keeping includes for providers that use their own bounce domain. They cost lookups and do nothing.
- Hand-flattening and forgetting. It works until the provider changes IPs.
- Using
ptr. It costs lookups and is discouraged by the RFC. - Fixing SPF and ignoring DKIM. With DKIM signed by your domain for every sender, an SPF problem no longer breaks DMARC.
How to verify it worked
dig +short TXT example.com
dig +short TXT news.example.com
Recount each record's lookups, or run it through our email checker. Then send a message through each sender to port25's verifier (check-auth@verifier.port25.com) and to mail-tester.com, and check that SPF reports pass, not permerror. From your own server:
swaks --to check-auth@verifier.port25.com --from hello@example.com \
--server mail.example.com --port 587 --tls \
--auth LOGIN --auth-user hello@example.com
In Gmail, "Show original" should show spf=pass and dmarc=pass. For the basics of record syntax, see how to set up an SPF record. If SPF now passes and mail still lands in spam, the cause is elsewhere: work through why are my emails going to spam.
If you would rather someone untangle a multi-provider SPF record, set up subdomains and align DKIM for each sender, that is our Deliverability Fix; see pricing.