"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:

  1. Publish SPF for the subdomain (news.example.com above), with only that provider's include.
  2. 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.
  3. Send a test and check smtp.mailfrom=news.example.com and spf=pass in Gmail's "Show original".
  4. 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=spf1 records 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.