Skip to content

SPF PermError: Too Many DNS Lookups — How to Fix It

Short answer

SPF PermError means your record needed more than 10 DNS-querying mechanisms to evaluate. RFC 7208 caps the total at 10 across the whole nested chain. Every include, a, mx, ptr, exists, and redirect counts; ip4, ip6, and all are free. Remove unused senders, split streams onto subdomains, or flatten.

Check it yourself
SPF Checker
Validate SPF syntax and count the 10-lookup limit live

A receiving mail server that hits this limit stops evaluating and returns permerror. Under most policies permerror is treated as "SPF did not pass" — which means the record you carefully published is doing nothing at all, and if your DMARC policy depends on SPF alignment, mail that should pass starts failing. The record still looks correct in the DNS zone, which is why this one takes people so long to spot.

SPF Checker
Validate SPF syntax and count the 10-lookup limit live

What exactly is the 10-lookup limit?

RFC 7208 section 4.6.4 says an SPF evaluation must not use more than 10 mechanisms and modifiers that cause DNS queries. The limit is on the *count of DNS-querying terms*, not on the number of queries a resolver makes, and it applies to the entire recursive evaluation — your record plus everything it pulls in, and everything those records pull in. There is no per-record budget. One include: that expands into six more includes has spent seven of your ten.

TermCounts toward the 10?Notes
include:Yes — 1 each, plus everything inside itThe single biggest source of overruns
a / a:hostYes — 1Resolves A/AAAA for the domain or named host
mx / mx:domainYes — 1The MX query is 1, but each returned MX name is then resolved; no more than 10 MX names may be processed
ptr / ptr:domainYes — 1RFC 7208 says do not publish this. Remove it.
exists:Yes — 1Often macro-driven, e.g. exists:%{i}._spf.mta.salesforce.com
redirect=Yes — 1, plus the target recordIgnored entirely if an all mechanism is present
ip4: / ip6:NoFree. This is why flattening works.
allNoJust the default result
exp=NoExplanation text is not counted
The initial lookup of your own SPF TXT record is not counted. Your budget starts at 10 *after* the resolver has fetched v=spf1 ... for the domain in the MAIL FROM.

How do nested includes eat the budget?

The cost of an include: is never obvious from your own record, because the vendor controls what is on the other end and can change it without telling you. Two real examples, both verifiable right now with dig:

# Cheap: one flat record, no nesting. Cost = 1 lookup.
dig +short TXT _spf.google.com
# "v=spf1 ip4:74.125.0.0/16 ip4:209.85.128.0/17 ip6:2001:4860:4864::/56 ... ~all"

# Expensive: include:mailgun.org expands into four more records.
dig +short TXT mailgun.org | grep spf1
# "v=spf1 include:_spf.mailgun.org include:_spf.eu.mailgun.org -all"
dig +short TXT _spf.mailgun.org
# "v=spf1 include:_spf1.mailgun.org include:_spf2.mailgun.org ~all"
Walking an include chain by hand with dig

That second chain costs 5 of your 10 lookups: mailgun.org, _spf.mailgun.org, _spf.eu.mailgun.org, _spf1.mailgun.org, _spf2.mailgun.org. Meanwhile include:sendgrid.net costs 2, because sendgrid.net itself ends with include:ab.sendgrid.net. A record that looks like four vendors is often eight or nine lookups before you have added a and mx.

v=spf1 a mx include:_spf.google.com include:sendgrid.net include:mailgun.org include:_spf.salesforce.com ~all
Looks like six terms. Costs 1 + 1 + 1 + 2 + 5 + 2 = 12. PermError.

The Salesforce entry is worth a second look: _spf.salesforce.com publishes v=spf1 exists:%{i}._spf.mta.salesforce.com -all, so the include costs 1 and the exists: inside it costs another 1. Macros are not free.

What is the void lookup limit?

There is a second, less well known cap in the same section of RFC 7208: void lookups. A void lookup is a DNS query made during SPF evaluation that returns NXDOMAIN, or NOERROR with zero answer records. The specification says implementations should limit these to two, and return permerror on the third. In practice this catches records that reference hostnames which no longer exist — a decommissioned a:mail.oldhost.example or an include: for a vendor whose SPF record was deleted when you cancelled the account.

  • A dead include: is a void lookup and still consumes one of the 10.
  • mx on a domain with no MX records is a void lookup.
  • Void lookups are why a record can pass a syntax check and still permerror in production — the syntax is fine, the targets are gone.
  • Run our SPF checker or a live dig on every hostname in the record; anything returning an empty answer needs removing.

How do you actually fix it?

1. Delete senders you no longer use

This is the fix that works and costs nothing. SPF records accumulate: a trial ESP from three years ago, a ticketing system that was replaced, a marketing tool nobody renewed. Before flattening anything, get a list of what genuinely sends as your domain. DMARC aggregate reports are the honest source — publish a record with a rua= address (the DMARC generator will build one) and read a week of reports before you touch SPF.

2. Do not add DKIM-only vendors to SPF

SPF authenticates the MAIL FROM / Return-Path domain, not the visible From: header. If a vendor sends with its own bounce domain and signs with a DKIM key on your domain, it does not belong in your SPF record at all — DKIM alignment alone satisfies DMARC. Plenty of overlong records are half full of vendors that never needed to be there. See how to find your DKIM selector to confirm a vendor is signing for you.

3. Move separate mail streams onto subdomains

Each domain gets its own 10-lookup budget, and DMARC alignment defaults to relaxed, meaning a subdomain of your organizational domain still aligns with a From: at the parent. Configure your ESP to use a custom Return-Path such as news.example.com or bounces.example.com, then publish a small SPF record on that subdomain with only that vendor in it. Your apex record shrinks to your corporate mail platform.

; apex — corporate mail only
example.com.            TXT  "v=spf1 include:_spf.google.com ~all"

; bulk marketing stream, own Return-Path, own budget
news.example.com.       TXT  "v=spf1 include:mailgun.org -all"

; transactional stream
mail.example.com.       TXT  "v=spf1 include:sendgrid.net -all"
Splitting streams by subdomain — three records, three separate 10-lookup budgets

4. Flatten — and understand what you are signing up for

Flattening means resolving an include: chain and pasting the resulting ip4:/ip6: ranges into your record. It works, because IP mechanisms cost nothing. The cost is maintenance: the vendor can change ranges at any time, and when they do your flattened record silently starts failing SPF for real mail. That failure mode is worse than permerror, because nothing in your zone looks wrong. If you flatten, treat it as a monitored job — re-resolve the source records on a schedule, diff, and alert on change. Never flatten by hand once and forget it.

Watch the size limits too. A single character-string in a TXT record is capped at 255 bytes, so long records must be published as multiple quoted strings that the resolver concatenates — amazonses.com does exactly this today. Keep the whole answer comfortably under 512 bytes so it survives UDP without truncation.

5. Drop ptr, and question a and mx

ptr is expensive, slow, and RFC 7208 explicitly advises against publishing it. Remove it. Then ask whether a and mx are earning their lookups: a authorizes your web server to send mail, which is only useful if it actually does, and mx authorizes your inbound mail servers, which for a hosted platform like Google Workspace or Microsoft 365 are already covered by the vendor include. Two lookups recovered for free is common. Confirm what your MX actually points at with the MX lookup first.

How do you verify the fix?

# 1. Read the published record exactly as a receiver sees it
dig +short TXT example.com | grep spf1

# 2. Confirm there is exactly ONE v=spf1 string (two = permerror, regardless of lookups)
dig +short TXT example.com | grep -c spf1

# 3. Walk each include target and count as you go
dig +short TXT _spf.google.com
dig +short TXT sendgrid.net | grep spf1

# 4. Confirm a subdomain stream resolves on its own
dig +short TXT news.example.com
Verifying an SPF record after edits

Then re-run the SPF checker, which counts the chain live rather than from a cached table, and send a real test message so you can read spf=pass in the header analyzer. Remember that resolvers will hold the old record for its TTL, so a fix is not visible everywhere immediately — check the DNS propagation checker if results disagree between test tools. Once SPF is under budget, revisit whether ~all or -all is right for you in SPF ~all vs -all.

Frequently asked questions

Does the 10-lookup limit apply per record or across the whole chain?
Across the whole evaluation. The receiver starts a counter when it begins checking your domain and increments it for every DNS-querying mechanism it encounters, including every one inside every nested include. Your record and all of its descendants share a single budget of 10.
Do ip4 and ip6 mechanisms count?
No. They require no DNS query, so you can publish as many as you like without touching the limit. This is exactly why flattening an include chain into IP ranges resolves a PermError — but it moves the problem from DNS to change management, since you now own keeping those ranges current.
What happens to my mail when SPF returns permerror?
The receiver treats SPF as not having passed. For DMARC that means the SPF side of alignment fails, so you are relying entirely on DKIM. If DKIM is also missing or unaligned and your DMARC policy is quarantine or reject, legitimate mail gets filed as spam or rejected outright.
Can I have two SPF records to get more lookups?
No. RFC 7208 requires exactly one v=spf1 TXT record per domain name; two matching records is itself a permerror, and it is a separate failure from the lookup limit. Merge them into one record. What you can do is publish separate records on separate subdomains and route different mail streams through them.
Is an SPF flattening service safe to use?
It is a real trade-off rather than a clean win. A hosted flattening service keeps the ranges current for you but inserts a third party into your authentication path, usually via a redirect or include pointing at their infrastructure. If you go that route, make sure you can see what they publish and that you get alerted when it changes.
How do I know which senders I can safely remove?
Publish DMARC with an rua address and collect aggregate reports for one to two weeks — longer if you have monthly billing runs or seasonal campaigns. The reports name every source IP sending as your domain and whether it aligned, which gives you an evidence-based list instead of a guess.

Last updated: 2026-09-01