Fixing 'SPF PermError: Too Many DNS Lookups', Worked Example
SPF permits at most 10 DNS-querying terms per evaluation. Every include, a, mx, ptr, exists and redirect counts, including those nested inside included records. Exceed 10 and receivers return permerror, which DMARC treats as a failure. Fix it by removing includes you don't need, moving senders to subdomains, or replacing hostnames with IP ranges.
The rule is in RFC 7208, section 4.6.4: "SPF implementations MUST limit the total number of those terms to 10 during SPF evaluation... If this limit is exceeded, the implementation MUST return 'permerror'." Most guides stop there. The useful part is knowing what each include actually costs, so we measured it.
What common includes cost
We counted the lookups behind each provider's published SPF include on 1 October 2026, using dig and a small script that walks the include tree. The cost is the include itself plus every lookup-causing term beneath it. These change whenever a provider edits its records, so re-check before relying on a number.
| Include | Lookups it costs | Why |
|---|---|---|
_spf.google.com (Google Workspace) |
1 | Flat list of IP ranges |
spf.protection.outlook.com (Microsoft 365) |
1 | Flat |
servers.mcsv.net (Mailchimp) |
1 | Flat |
amazonses.com (Amazon SES) |
1 | Flat |
mail.zendesk.com |
1 | Flat |
spf.mandrillapp.com |
1 | Flat |
spf.brevo.com |
1 | Flat |
spf.mtasv.net (Postmark) |
1 | Flat |
mktomail.com (Marketo) |
1 | Flat |
sendgrid.net |
2 | Nests ab.sendgrid.net |
_spf.salesforce.com |
2 | Uses an exists: macro |
sparkpostmail.com |
2 | One nested include |
zohomail.com |
2 | One nested include |
mailgun.org |
5 | Four nested includes |
zoho.com |
5 | Four nested includes |
Most single-provider includes cost one lookup. The expensive ones are providers that nest several levels deep. Two of those in one record use 10 of your budget between them.
A real record over the limit
Counting the SPF record for gitlab.com on 1 October 2026 gives 13. We chose it because it is a public record from a well-run company that shows exactly how records drift over the limit: one vendor at a time. Here is the top-level record, with the running count in order of evaluation:
v=spf1 include:mail.zendesk.com # 1
include:_spf.google.com # 2
include:mktomail.com # 3
include:_spf.salesforce.com # 4, plus exists: inside = 5
include:_spf-ip.gitlab.com # 6
a:zgateway.zuora.com # 7
include:mailgun.org # 8
-> include:_spf.mailgun.org # 9
-> include:_spf1.mailgun.org # 10
-> include:_spf2.mailgun.org # 11
-> include:_spf.eu.mailgun.org # 12
include:_spf.sendergen.com # 13
ip4:35.80.141.6/32 ip4:44.229.121.55/32 -all
Nothing here is unusual. Each line is a legitimate vendor, and the overrun comes almost entirely from mailgun.org, which on its own costs five.
Why it doesn't fail all the time
The limit is applied during evaluation, and SPF evaluates left to right, stopping at the first match. Following the RFC strictly:
- Mail sent from Google's IP ranges matches at lookup 2 and passes. The limit is never reached.
- Mail from Mailgun's first block of ranges (
_spf1) matches at lookup 10 and passes, just. - Mail from Mailgun's second block (
_spf2) needs lookup 11, so the receiver returnspermerror. - Mail from the last include never gets evaluated at all.
So the same record passes for some senders and fails for others. That is why "too many lookups" problems often present as one tool's mail failing DMARC while everything else looks fine. A static checker that counts the whole tree, like the script below, reports 13 whichever server sent the message. Either way, you can't rely on a record over 10.
Count your own record
Run this anywhere with Python 3 and dig. It prints the tree and the total.
import subprocess, re, sys
def spf(name):
out = subprocess.run(["dig", "+short", "TXT", name], capture_output=True, text=True).stdout
recs = ["".join(re.findall(r'"((?:[^"\\]|\\.)*)"', l)) for l in out.splitlines()]
return [r for r in recs if r.lower().startswith("v=spf1")]
def count(name, depth=0):
recs = spf(name)
print(" " * depth + name + ": " + (recs[0] if recs else "NO SPF RECORD"))
if not recs:
return 0
total = 0
for term in recs[0].split()[1:]:
t = term.lstrip("+-~?").lower()
if t.startswith("include:"):
total += 1 + count(t.split(":", 1)[1], depth + 1)
elif t.startswith("redirect="):
total += 1 + count(t.split("=", 1)[1], depth + 1)
elif t in ("a", "mx", "ptr") or t.startswith(("a:", "a/", "mx:", "mx/", "ptr:", "exists:")):
total += 1
return total
print("TOTAL:", count(sys.argv[1]))
python3 spf_count.py yourdomain.com. Or use our SPF checker, which does the same count without the terminal.
Two other limits in the same RFC section catch people out:
- Void lookups. RFC 7208 says implementations "SHOULD limit 'void lookups' to two", meaning lookups that return no records or a non-existent domain. An include pointing at a vendor you stopped using, whose record has since been deleted, uses up one of these.
- The
mxmechanism. Eachmxterm counts as one lookup, and the MX hosts it returns may not resolve to more than 10 addresses.ptris marked "do not use" in the RFC.
And one that isn't a lookup limit but produces the same permerror: publishing two separate SPF records on one domain. Merge them into one.
How to get back under 10
Work through these in order. The first two fix most records without any ongoing maintenance.
1. Remove includes that do nothing for you
SPF checks the domain in the envelope sender (the Return-Path, or MAIL FROM), not the From address your recipients see. Many email platforms put their own bounce domain in the envelope sender by default. If yours does, receivers check SPF against the platform's domain, and the include in your record is never consulted.
To check, send yourself a message through each platform and look at the Return-Path header:
| Return-Path shows... | Do you need the include? |
|---|---|
bounce@yourdomain.com or a subdomain of yours |
Yes, SPF is checked against your domain |
bounces+123@em.platform.com or similar |
No, remove it. DMARC alignment comes from DKIM instead |
That second case only works if the platform signs DKIM with your domain, so confirm that with our DKIM checker before deleting anything. Also delete includes for tools you no longer use. Old includes are the most common source of void lookups.
2. Move senders to subdomains
Each subdomain gets its own SPF record and its own budget of 10. Put marketing on news.yourdomain.com and support on help.yourdomain.com, configure the platform to use that subdomain as its custom return-path domain, and your root record only needs to cover your mailboxes. DMARC alignment still works, because relaxed alignment (the default) treats subdomains of the same organisational domain as aligned.
3. Replace hostname lookups with IP ranges you control
An a: or mx that points at your own server costs a lookup every time. If the IP is static and yours, ip4:203.0.113.10 costs nothing.
4. Flatten, carefully
SPF flattening replaces includes with the IPs they currently resolve to. It reliably gets you under the limit and reliably goes stale, because providers change their ranges without telling you. If you flatten, do it with a service or a scheduled script that re-resolves and republishes, and don't flatten providers like Salesforce whose record uses an exists: macro. That isn't a list of IPs, so there is nothing to flatten.
A fix, start to finish
Here is a hypothetical company using the costs measured in the table above. Its mailboxes are on Google Workspace, marketing goes through SendGrid, product notifications through Mailgun, and sales and support use Salesforce and Zendesk:
v=spf1 include:_spf.google.com include:sendgrid.net include:mailgun.org
include:_spf.salesforce.com include:mail.zendesk.com ~all
| Include | Cost |
|---|---|
_spf.google.com |
1 |
sendgrid.net |
2 |
mailgun.org |
5 |
_spf.salesforce.com |
2 |
mail.zendesk.com |
1 |
| Total | 11 |
One over. Two changes bring it down without flattening anything:
- Product notifications move to
notify.example.com, with Mailgun configured to use that subdomain in the envelope sender. The subdomain gets its own record,v=spf1 include:mailgun.org ~all, costing 5 of its own 10. - The root record drops
include:mailgun.org, and falls to 6.
That leaves four lookups of headroom on the root domain for the next tool someone signs up for, and keeps Mailgun's nested includes somewhere they can't push anything else over the limit.
After the fix
Re-count, then send a test through every platform you use and check Authentication-Results for spf=pass. Remember that SPF is one of three checks. DMARC passes if either SPF or DKIM passes and aligns, so a clean DKIM setup is what keeps a temporary SPF problem from becoming a delivery problem. Our SPF generator builds a fresh record if you would rather start again than edit the old one.
The record you have now was built one vendor at a time. Count it whenever you add one.
Common questions
Which SPF mechanisms count towards the 10 DNS lookup limit?
Under RFC 7208, the include, a, mx, ptr and exists mechanisms and the redirect modifier each count as one lookup, and lookups inside included records count too. The ip4, ip6 and all mechanisms do not count, which is why replacing hostnames with IP ranges reduces the total.
What happens when an SPF record exceeds 10 lookups?
The receiving server must return a permerror result instead of pass or fail. Because the limit is applied during evaluation, mail that matches an early mechanism can still pass while mail from a sender listed later fails, which makes the problem look intermittent.
Is SPF flattening a good fix?
It works but it is fragile. Flattening replaces includes with the IP ranges they currently resolve to, so when a provider changes its IPs your record silently goes stale. Use it only with a service or script that re-flattens automatically, and try removing unnecessary includes first.
Do I need an include for every email platform I use?
Only for platforms that send with your domain in the envelope sender (Return-Path). Many platforms use their own bounce domain there, in which case SPF is checked against their domain, not yours, and your include does nothing. Those platforms rely on DKIM for DMARC alignment.
Verify unlimited addresses for $29.99/month
Real SMTP mailbox checks. No credits, no per-email fees.
Get Started