Email authentication

SPF's 10 DNS lookup limit: why SPF breaks and how to fix it

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:

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

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.

Count your SPF lookups

Free, no sign-up: a 0 to 100 score and the exact record for each fix in about three seconds.

Related guides

Check your domain, then watch it

Add a domain to a free account and we re-check it every day and read its DMARC reports. Free for one domain.