SPF permerror: how to fix "too many DNS lookups"
Last updated: 26:10:2026
What the error means
You see it in a checker, a DMARC report or a message header: spf=permerror, often with a note such as "too many DNS lookups". A permerror means the receiver couldn't evaluate your SPF record at all - not that the sending server was unauthorized, but that the record itself is broken. For DMARC that is simply "SPF did not pass", and it happens to every message, including those from servers that are listed correctly.
Messages that also carry a valid, aligned DKIM signature still pass DMARC, which is why this problem often goes unnoticed for months. But any sender that relies on SPF alone - and any receiver that weighs SPF on its own - is affected. This guide goes deeper than the general SPF guide: exactly what counts, how to count it, and how to get back under the limit.
The two limits in RFC 7208
Section 4.6.4 of RFC 7208, the SPF specification, sets both limits:
- 10 DNS-querying terms. The
include,a,mx,ptrandexistsmechanisms and theredirectmodifier together may appear at most 10 times during one SPF evaluation. Exceeding this must producepermerror. - 2 void lookups. A void lookup is a DNS query that returns no records - either "this name doesn't exist" (NXDOMAIN) or "the name exists but has no record of this type". Receivers should allow at most two; a third produces
permerror.
The limits exist so that one email can't make a receiver run dozens of DNS queries, which would make SPF a tool for flooding DNS servers. Treat them as hard limits: a receiver that follows the RFC returns permerror the moment you cross one, and you can't know which receivers are more lenient.
What counts and what doesn't
| Term | Counts | Notes |
|---|---|---|
include: | Yes, 1 | Plus every counting term inside the included record, and inside the records it includes |
a / a: | Yes, 1 | One lookup of A/AAAA for the domain |
mx / mx: | Yes, 1 | Plus a separate rule: the MX hosts it finds may need at most 10 address lookups, or the mechanism itself is a permerror |
ptr | Yes, 1 | Deprecated - RFC 7208 says it SHOULD NOT be published. Remove it |
exists: | Yes, 1 | Used by some providers with macros |
redirect= | Yes, 1 | But ignored completely if the record contains an all mechanism |
ip4: / ip6: / all | No | No DNS query - these are free |
exp= | No | Only queried later, to build an explanation for a rejection |
include:a / a:mx / mx:ptrexists:redirect=ip4: / ip6: / allexp=Fetching your own domain's SPF record to start with isn't a term and doesn't count; fetching each included record is what the include: itself counts for. Two details are often misunderstood:
- Nested includes add up.
include:doesn't copy the other record's text into yours, but evaluating it runs that record's terms - so a provider whose SPF record contains three more includes costs you four lookups, not one. - An include of a domain without an SPF record is its own permerror, regardless of the count. That's what happens when a provider retires an old SPF hostname and you're still including it.
"But it worked for some messages"
The limit applies to lookups made during evaluation, and evaluation stops at the first mechanism that matches. A message from a server listed early in the record may be authorized before the 11th lookup is ever needed, while a message from a server listed late fails. That makes an over-limit record worse, not better: it fails unpredictably. Count the worst case - every term, every nested include - and keep that under 10.
How to count your lookups
Start with your own record, then follow each include: and redirect= and count the terms in every record you reach. Here's an example domain with two services:
The domain's own record
example.com. TXT "v=spf1 mx include:_spf.crm.example include:spf.newsletter.example ~all"The records it includes
_spf.crm.example. TXT "v=spf1 include:_s1.crm.example include:_s2.crm.example -all"
_s1.crm.example. TXT "v=spf1 ip4:198.51.100.0/24 -all"
_s2.crm.example. TXT "v=spf1 ip4:203.0.113.0/24 -all"
spf.newsletter.example. TXT "v=spf1 a include:servers.newsletter.example ~all"
servers.newsletter.example. TXT "v=spf1 ip6:2001:db8:100::/48 -all"| Record | Counting terms | Lookups |
|---|---|---|
example.com | mx / include / include | 3 |
_spf.crm.example | include / include | 2 |
_s1 / _s2.crm.example | only ip4: | 0 |
spf.newsletter.example | a / include | 2 |
servers.newsletter.example | only ip6: | 0 |
| Total | 7 of 10 |
example.commx / include / include_spf.crm.exampleinclude / include_s1 / _s2.crm.examplespf.newsletter.examplea / includeservers.newsletter.exampleSeven is fine today, but providers change their own records without telling you - one more include in _spf.crm.example and this domain is at eight. Leave yourself some margin. Doing this by hand with dig TXT name +short works, but it's tedious; the email authentication checker walks the whole include tree for you, shows how many lookups each record uses, counts void lookups, and flags includes that point at a domain without an SPF record.
How to get under the limit
1. Remove what you no longer use
The usual culprit is history: a newsletter tool you left two years ago, a CRM trial, a hosting company's mail you never used. If you're not sure whether a service still sends mail as your domain, your DMARC aggregate reports list the IP addresses that send mail with your domain - a service whose addresses never show up there is a candidate for removal.
2. Replace a, mx and ptr with ip4: and ip6:
a and mx are convenient, but each costs a lookup, and mx is often simply wrong: the servers that receive your mail aren't necessarily the ones that send it - with a hosted provider they usually aren't. For servers you run yourself, list their addresses with ip4: and ip6:, which cost nothing. Remove ptr outright.
3. Check which services really need to be in your record
SPF is checked against the domain of the envelope sender (MAIL FROM, visible as Return-Path), not the From: address. Many sending services use their own domain, or a bounce subdomain of yours that they host, as the envelope sender. For their mail, your apex SPF record is never consulted, so their include there is a wasted lookup. Open a message sent through the service and look at its Return-Path: if it isn't your domain, the include doesn't help that service's mail. DMARC alignment for those messages then comes from DKIM - make sure the service signs with your domain.
4. Move senders to subdomains
Every hostname has its own SPF record with its own 10-lookup budget. Send marketing mail from news.example.com, transactional mail from a tool that uses mail.example.com, and each subdomain's record only includes the services that actually use it. It also keeps the reputation of bulk mail apart from your everyday mail. Remember that subdomains don't inherit SPF: each one that appears as an envelope sender needs its own record.
5. Remove duplicates
Two includes for the same provider - an old and a new hostname, or a regional variant next to the global one - are common after migrations. Keep the one the provider currently documents.
SPF flattening: possible, but risky
"Flattening" replaces includes with the IP addresses they currently resolve to, so they cost no lookups. It works, at a price:
- It goes stale silently. Providers add and change sending IPs whenever they need to. Your flattened copy doesn't follow, and mail from the new addresses starts failing SPF without any error on your side.
- Records get long. Large providers have many ranges. RFC 7208 (section 3.4) recommends keeping SPF answers small enough to fit a 512-byte DNS response, and each TXT string holds at most 255 characters, so long lists have to be split across strings.
- Automated flattening services keep the list up to date for you - but you then include their hostname, which means trusting a third party with the list of servers allowed to send as your domain.
Flatten only what you control - your own servers' addresses - and keep providers as includes. If you must flatten a provider, automate it and re-check regularly.
Fixing void lookups
Void lookups come from terms that point at nothing: an include: of a domain that no longer exists, an a: for a hostname that was deleted, an mx for a domain without MX records. One or two don't break anything, but they are signs of rot - remove them before a third one turns the whole record into a permerror.
After the fix
Publish the shortened record as one TXT record - a domain may have only one record starting with v=spf1 - wait for the old one's TTL, and check again. Then send a message to an external mailbox and confirm spf=pass in its Authentication-Results header. If you send through AnyMail, its part of your record is a single include:anymail.tech; that record currently lists only IP addresses, so it costs one lookup.
Let AnyMail handle these records for you
AnyMail generates the MX, SPF, DKIM and DMARC records for your domain, checks every one of them automatically and tells you exactly which record to fix - then hosts your inboxes, operated from the EU.