SPF limits every check to 10 DNS lookups. If a receiver needs an 11th lookup to evaluate your record, it stops and returns permerror, a permanent error. SPF does not pass, no matter how correct your IP addresses are. The limit is in section 4.6.4 of RFC 7208.
You usually find out through a symptom, not a warning: a DMARC report with SPF failures, a "too many DNS lookups" error in a checker, or legitimate mail landing in spam. This post explains what counts toward the 10, how to count your own record, and how to fix it, starting with the safest change.
Why a permerror hurts
SPF checks the envelope sender domain (the MAIL FROM address, which shows up later as Return-Path) and the HELO name. It does not check the visible From address. A permerror means the receiver cannot tell whether the sending server is authorized.
DMARC (RFC 7489) passes if either aligned SPF or aligned DKIM passes. Aligned SPF means SPF passes and the Return-Path domain matches the From domain. Relaxed alignment, the default, accepts the same organizational domain. Strict alignment needs an exact match.
When SPF returns permerror, DMARC has to rely on aligned DKIM. If DKIM also fails or is not aligned, DMARC fails. With p=quarantine or p=reject, that mail goes to spam or is rejected. Some receivers also treat permerror harshly even without DMARC. If you are still on p=none, nothing is blocked yet, but you cannot safely move to enforcement until this is fixed. See moving DMARC from p=none to p=reject.
What counts toward the limit
The terms that cause DNS lookups are the include, a, mx, ptr, and exists mechanisms and the redirect modifier. Their total during one evaluation must not exceed 10.
| Term | Counts toward 10? | Notes |
|---|---|---|
include: |
Yes, 1 each | Plus everything inside the included record, recursively |
a |
Yes, 1 | The address lookup for the host |
mx |
Yes, 1 | The MX query counts. The address lookups for the MX hosts do not, but one mx may resolve at most 10 MX names (more is a permerror) |
ptr |
Yes, 1 | Deprecated: RFC 7208 says it SHOULD NOT be used |
exists: |
Yes, 1 | Often used for custom macros |
redirect= |
Yes, 1 | Plus everything in the target record |
ip4: / ip6: |
No | Literal addresses and ranges need no lookup |
all |
No | The ending needs no lookup |
exp= |
No | Not counted |
A second limit is easy to miss. Void lookups, meaning queries that return NXDOMAIN or an empty answer, should be limited to 2, the default RFC 7208 recommends, and most receivers return permerror on the third. This usually happens when you include a provider record that has since been deleted.
How nesting adds up
Includes count recursively. An include whose record contains 3 more includes costs 4 in total: 1 for the include itself and 3 for the ones inside it. Those inner records can have includes of their own.
This is why SPF breaks without anyone touching DNS. You include a provider's record, and the provider later adds a nested include for a new data center or a new product. Your record is unchanged, but your count went up. A record that was at 9 last year is at 11 today.
Some practical consequences:
- You cannot judge a record by its length. A short record with four includes can cost more than a long one made of
ip4ranges. - You do not control the count of your includes. Leave headroom of 2 or 3 lookups when you can.
- Each name has its own budget. The 10 applies to the record being evaluated, starting from the domain in the Return-Path. A subdomain with its own SPF record gets a fresh 10.
Count your own lookups
Start with your domain's record. Use dig:
dig +short TXT example.com
Or DNS over HTTPS, if dig is not available:
curl -s 'https://dns.google/resolve?name=example.com&type=TXT' | jq -r '.Answer[].data'
Find the line that starts with v=spf1. Then run the same query for every include: and redirect= target, and repeat for any includes you find inside those. Here is a made-up record:
example.com. TXT "v=spf1 include:_spf.crm.example include:mail.newsletter.example include:_spf.helpdesk.example a mx -all"
Query each include and follow it down:
_spf.crm.example. TXT "v=spf1 include:_netblocks.crm.example include:_netblocks2.crm.example -all"
mail.newsletter.example. TXT "v=spf1 include:spf.relay.newsletter.example ~all"
_spf.helpdesk.example. TXT "v=spf1 ip4:192.0.2.0/24 ip6:2001:db8::/32 -all"
Now count, adding one for every term that triggers a lookup:
include:_spf.crm.example 1
include:_netblocks.crm.example 1
include:_netblocks2.crm.example 1
include:mail.newsletter.example 1
include:spf.relay.newsletter.example 1
include:_spf.helpdesk.example 1 (ip4 and ip6 inside are free)
a 1
mx 1
-all 0
---------------------------------------------
Total 8 of 10
That works, with 2 to spare. Now suppose someone adds include:_spf.billing.example, and that record has 3 nested includes. That is 4 more lookups, for a total of 12. Every message evaluated against this record gets a permerror.
Counting by hand is tedious and easy to get wrong, especially when includes are four levels deep. The free check on DNS Autopilot does it for you: it reads the SPF record, follows every include, counts the lookups against the limit of 10, and shows how the record ends.
Fixes, from safest to riskiest
Work down this list. The first four do not change who is authorized to send. The fifth does, and it needs care.
1. Remove includes for services you no longer use
Old CRMs, retired newsletter tools, a trial of a helpdesk: each one may still be in your record. Check the vendor's dashboard or ask the team. If nothing sends through a service, delete its include. Also delete includes that point to provider records that no longer exist, since they add void lookups.
2. Find out which services actually use your domain in the Return-Path
Many sending services use their own bounce domain as the Return-Path by default. SPF is then checked against the provider's domain, not yours. Your SPF record plays no part for that mail, and your include for the service is dead weight.
Those services can still pass DMARC through DKIM. In each service, set up DKIM signing with your own domain. This is often called domain authentication or sender authentication. Once the message is signed with d=example.com, DKIM aligns with your From domain, and you can drop the include.
To check, send a real message from the service to a mailbox you control and view the original headers. Look at two places:
Return-Path: <bounce-8123@mail.newsletter.example>
Authentication-Results: mx.receiver.example;
spf=pass smtp.mailfrom=mail.newsletter.example;
dkim=pass header.d=example.com;
dmarc=pass header.from=example.com
Here SPF was checked against mail.newsletter.example, not example.com, so the include in your record does nothing for this stream. DMARC passes because of DKIM. Only remove the include after you confirm DKIM passes and aligns for that service.
3. Give a heavy sender its own subdomain
If a service must use your domain in the Return-Path, give it a subdomain such as bounce.example.com as a custom Return-Path, and publish a separate SPF record there:
bounce.example.com. TXT "v=spf1 include:mail.newsletter.example -all"
That name has its own budget of 10 lookups, and your main record loses the include. Relaxed alignment still matches, because bounce.example.com and example.com share an organizational domain. The setting for this is usually called custom Return-Path, custom bounce domain, or custom MAIL FROM in the sending service.
4. Replace a and mx with literal addresses
If a or mx points at servers that are yours and rarely change, list their addresses with ip4 and ip6 instead. Each swap saves one lookup, and it saves more if your mx is a set of hosts. If your web server does not send mail, drop a entirely. If your MX hosts only receive mail, which is common with hosted mailboxes, you may not need mx either.
Only do this for addresses you control. If the addresses can change without you knowing, a literal list will go stale.
5. Flatten, if you must
Flattening replaces includes with the IP ranges they resolve to. It works: ip4 and ip6 cost no lookups. The risk is that providers change their ranges without notice. A record you flattened by hand keeps listing the old addresses, and mail from the new ones starts failing with no error to point at the cause.
If you flatten, use tooling that re-resolves the includes on a schedule and republishes the record. Even then, prefer fixes 1 to 4. Flattened records also get long, so they may hit the 255-character limit on a single TXT string (see below).
What not to do
- Do not publish two SPF records. Two
v=spf1TXT records on one name give permerror. Merge them into one record. Splitting a long record into two does not double your budget. - Do not use
+all. It authorizes every server on the internet to send as your domain. - Do not use
ptr. It is slow, unreliable, and deprecated by RFC 7208. It also costs a lookup. - Do not keep dead includes "just in case." Each one is a lookup you may need later.
Long records and the 255-character limit
This is a separate problem from the lookup limit, and it often shows up at the same time. A single string inside a TXT record can hold at most 255 characters. A longer SPF record has to be split into several quoted strings within one record:
example.com. TXT "v=spf1 ip4:192.0.2.0/24 ip4:198.51.100.0/24 ip4:203.0.113.0/24 " "include:_spf.crm.example -all"
Receivers join the strings without adding spaces, so put the space at the end of one string or the start of the next. Many DNS hosts split the strings automatically. If yours does not, you will see an error when you save the record. Splitting strings is fine. Publishing a second record is not.
Choosing the ending
The ending of your record tells receivers what to do with mail from servers you did not list:
| Ending | Meaning |
|---|---|
-all |
Fail: not authorized |
~all |
Softfail: probably not authorized |
?all |
Neutral: no statement |
+all |
Pass everything: never use this |
Once DMARC is at p=quarantine or p=reject, the choice between -all and ~all matters much less, because DMARC decides what happens to the message. Either is fine. ?all is weak and +all defeats the purpose of SPF.
Where DNS Autopilot fits
The free check counts your SPF lookups against the limit of 10, checks how the record ends, and gives you the exact record to publish for each fix above. It needs no sign-up. It also looks at DKIM, DMARC, MX, MTA-STS, DNSSEC, and other records, and gives the domain a score from 0 to 100.
The count can drift on its own, so a one-time check is not enough. A free account re-checks your domain every day, which catches a provider pushing you over 10 before your mail fails. If you send DMARC reports to DNS Autopilot, from Gmail or a report address, it also shows which senders fail SPF or DKIM, so you know which services to move to DKIM or a subdomain before you delete anything.
FAQ
What is the SPF 10 DNS lookup limit?
It is the maximum number of DNS-causing terms a receiver will process while evaluating one SPF record. The terms are include, a, mx, ptr, exists, and redirect, counted across all nested records. Going over gives a permerror, and SPF does not pass.
What happens when SPF has too many DNS lookups?
SPF returns permerror, which counts as a failure to authenticate. DMARC then depends on aligned DKIM. If DKIM also fails, DMARC fails, and with p=quarantine or p=reject the message is sent to spam or rejected.
Do ip4 and ip6 count toward the limit?
No. ip4, ip6, and all need no DNS query, so they cost nothing. The exp modifier is not counted either. This is why replacing an include with literal addresses reduces your count.
Can I have two SPF records to get more lookups?
No. A domain must have exactly one SPF record, and two v=spf1 TXT records on the same name give permerror. To get a bigger budget, move a sender to a subdomain with its own SPF record, since each name has its own 10 lookups.
Is SPF flattening a good fix?
It works, but it is the least safe option. Providers change their IP ranges without notice, so a record you flattened by hand silently breaks. Remove unused includes, switch to DKIM, or use a subdomain first, and flatten only with tooling that re-resolves on a schedule.
Why did my SPF record break when I changed nothing?
Includes count recursively, and the providers you include edit their own records. If a provider adds a nested include, your total goes up with no change on your side. Re-checking your record regularly is the only way to catch this early.