SPF permerror: too many DNS lookups — how to fix it
Why SPF fails with permerror when a record needs more than 10 DNS lookups, how to count them, and which fixes to apply first without breaking legitimate mail.
By Exmoor Software6 min read
When a receiver needs more than ten DNS lookups to evaluate your SPF record, it stops and returns permerror — a permanent error. You see it as spf=permerror in the Authentication-Results header of a received message, or as a “too many DNS lookups” warning in an SPF checker. Many receivers treat permerror like a failed check, and DMARC never counts it as a pass, so legitimate mail can end up in spam or be rejected.
The limit comes from RFC 7208, section 4.6.4, and protects DNS from records that would make every receiver send dozens of queries per message. You can’t ask receivers to raise it: the record has to get smaller.
What counts toward the limit
Only terms that make the receiver query DNS count:
| Term | Counts | Note |
|---|---|---|
include: | 1 each | plus everything inside the included record |
a, a:host | 1 each | |
mx, mx:domain | 1 each | its mail servers are resolved separately (see below) |
ptr | 1 each | should not be used at all |
exists: | 1 each | |
redirect= | 1 | plus everything in the target record |
ip4:, ip6:, all | no | |
exp= | no | only looked up after a fail |
The limit is 10 for the whole evaluation, including every nested include. Fetching your own record at the start is not a term and doesn’t count.
Two more limits from the same section apply on top:
- MX hosts. The mail servers behind a single
mxterm may need at most ten address lookups; beyond that the term returns permerror. - Void lookups. A query that returns no data — the name doesn’t exist, or has no record of the requested type — is a void lookup. Receivers should allow at most two; the third produces permerror. They usually come from an include for a service that no longer exists or an
a:for a deleted host.
Why the error can look random
Receivers evaluate the record from left to right and stop at the first match (section 4.6.2), counting lookups as they go. A message from a server matched by an early term may pass before the limit is reached, while a message from a server listed near the end fails. Microsoft’s SPF documentation describes the same effect. That is why the SPF checker counts the worst case — a sender that matches nothing until the end of the record.
How to count
This record has only five terms at the top level:
example.com. TXT "v=spf1 mx a include:_spf.mailhost.example include:spf.crm.example include:bounce.newsletter.example -all"
_spf.mailhost.example. TXT "v=spf1 include:_ip4.mailhost.example include:_ip6.mailhost.example ~all"
spf.crm.example. TXT "v=spf1 ip4:198.51.100.0/24 include:spf.cloud.example -all"
spf.cloud.example. TXT "v=spf1 ip4:203.0.113.0/24 ip6:2001:db8::/32 -all"
bounce.newsletter.example. TXT "v=spf1 a:out1.newsletter.example a:out2.newsletter.example mx ~all"
Follow every include and count the terms that need DNS (assume the _ip4 and _ip6 records contain only IP ranges):
| # | Term | Found in |
|---|---|---|
| 1 | mx | example.com |
| 2 | a | example.com |
| 3 | include:_spf.mailhost.example | example.com |
| 4 | include:_ip4.mailhost.example | _spf.mailhost.example |
| 5 | include:_ip6.mailhost.example | _spf.mailhost.example |
| 6 | include:spf.crm.example | example.com |
| 7 | include:spf.cloud.example | spf.crm.example |
| 8 | include:bounce.newsletter.example | example.com |
| 9 | a:out1.newsletter.example | bounce.newsletter.example |
| 10 | a:out2.newsletter.example | bounce.newsletter.example |
| 11 | mx | bounce.newsletter.example |
Eleven: one too many. Six of them sit in records you don’t control, and providers change their records without telling you — a record that counted nine last month can count eleven today.
Fixes, in this order
1. Remove what you don’t use
Go through every include and ask two questions.
Does this service still send mail for you? Old newsletter tools, a previous helpdesk, the mailbox provider you migrated away from — remove them. Dead includes are also the usual source of void lookups.
Does it send with your domain in the envelope sender? SPF is checked for the Return-Path domain, not for the From address. Many services use their own bounce domain, or a return-path subdomain of yours with its own SPF record; their include in your root record then does nothing. Amazon SES, for example, uses a subdomain of amazonses.com by default, and with a custom MAIL FROM domain the include:amazonses.com goes into the record of that subdomain. Open a real message from each service and check the Return-Path — the email header analyzer shows it next to the SPF result.
In the example, if the newsletter service sends with its own bounce domain, include:bounce.newsletter.example can go, which saves four lookups. SPF then passes for the service’s domain, so for DMARC that mail relies on DKIM signed with your domain.
2. Replace a and mx with ip4 and ip6
RFC 7208 calls a record that lists addresses directly the “best record” and one using mx the “expensive record” (section 10.1.1). If your own servers have fixed addresses:
; before: 2 lookups for your own servers
v=spf1 mx a include:_spf.mailhost.example -all
; after: 0 lookups for your own servers
v=spf1 ip4:192.0.2.25 ip4:192.0.2.80 include:_spf.mailhost.example -all
Also check whether those hosts send mail at all: a authorizes the address of your website, and mx your inbound mail servers. When your mailbox provider sends your outgoing mail, both are often leftovers. The trade-off: when a server’s address changes, you have to update SPF by hand.
3. Remove ptr
RFC 7208 says ptr should not be published (section 5.5): it is slow, less reliable when DNS has problems, and puts load on the reverse DNS servers. Replace it with the actual address ranges.
4. Move bulk and third-party senders to subdomains
SPF is looked up for the exact envelope domain, so every subdomain has its own record and its own budget of ten lookups. Send newsletters from news.example.com and let the marketing platform use it as the return-path:
example.com. TXT "v=spf1 ip4:192.0.2.25 include:_spf.mailhost.example -all"
news.example.com. TXT "v=spf1 include:bounce.newsletter.example -all"
Microsoft recommends subdomains for services you don’t control directly, which also keeps their reputation apart from your staff’s mail. In DMARC’s default relaxed mode, an SPF pass for news.example.com still aligns with a From address at example.com, as long as example.com publishes a DMARC record — see SPF vs DKIM vs DMARC for alignment by example.
5. Flatten only as a last resort
“Flattening” replaces includes with the IP addresses they resolve to today. It brings the count down, but:
- providers change their addresses without notice, so a flattened record silently goes stale and legitimate mail starts failing;
- the record grows: a TXT string holds at most 255 characters, and RFC 7208 recommends keeping the DNS answer under about 450 octets so it fits in one UDP packet (section 3.4);
- Microsoft advises against flattening
include:spf.protection.outlook.com, because its addresses change often.
If you must flatten, do it only for providers that publish stable, documented ranges, and automate the updates.
What doesn’t help
- A second SPF record. With two
v=spf1records on one name, every evaluation ends in permerror (section 4.5). - Hiding includes in your own sub-record, such as
include:_spf.example.comthat contains the same includes. Nested lookups still count. - Changing
-allto~all. It changes the result for unlisted servers, not the number of lookups. - Changing the TTL. Caching affects how often DNS is asked, not how many terms are evaluated.
Check the result
- Run the SPF checker on your domain and on every subdomain you send from. It follows every include and redirect, shows the include tree with the lookup and void lookup counts, and labels problems inside providers’ records that you can’t change yourself.
- Send a test message from each service to an external mailbox and paste the headers into the email header analyzer. You want
spf=passfor the envelope domain you expect. - Give it time: resolvers may keep serving the old record until its TTL expires.
For the basics of how SPF is evaluated, see What is SPF and how does it work?