SPF PermError "too many DNS lookups": fix the 10-lookup limit

"SPF PermError: too many DNS lookups" means that checking your domain's SPF record needs more than 10 DNS-querying terms, the hard limit in RFC 7208, so receivers stop evaluating it and report a permanent error instead of a pass. Every message whose Return-Path uses that domain loses SPF. The fix is to count the lookups, including the nested ones hidden inside each include:, then remove dead entries, replace your own servers with ip4:/ip6:, and move third-party senders to their own subdomains.
include, a, mx, ptr, exists and redirect count toward the limit of 10, and every include counts everything nested under it; ip4, ip6 and all are free. A typical stack of Google Workspace, Mailgun, a helpdesk and a CRM can reach 17 lookups on one record. Check the Return-Path of real messages first: many includes cover senders whose envelope never uses your root domain. Give each bulk sender its own subdomain with its own SPF record and its own budget of 10; flatten to IP addresses only for vendors with stable, documented ranges.What counts toward the SPF 10-lookup limit?
Six terms count: the include, a, mx, ptr and exists mechanisms and the redirect modifier, and once evaluation needs more than 10 of them the result must be permerror. The ip4, ip6 and all mechanisms cost nothing (RFC 7208, section 4.6.4).
The limit applies to the whole evaluation, not to the line you see in your DNS panel. Microsoft puts it plainly: an include that points to a record with three more includes costs four lookups, so "having fewer than 10 include: statements doesn't guarantee fewer than 10 DNS lookups" (Microsoft Learn: Set up SPF). Google's own help page simplifies this to "up to 10 include tags", which is only true when none of them nests further (Google Workspace: Set up SPF).
A permanent SPF error: the record is broken in a way that retrying will not fix (too many lookups, a syntax error, two SPF records on one name, an include that points to a name without an SPF record). Receivers treat it as an SPF failure, and DMARC cannot use SPF for that message.
| Term | Counts toward 10 (lookups) | Extra rule |
|---|---|---|
include: | 1 + everything inside it | Target without an SPF record = permerror |
redirect= | 1 + everything inside it | Ignored when the record has all |
a, a:host | 1 | — |
mx, mx:host | 1 | At most 10 MX hosts may be resolved, otherwise permerror |
ptr | 1 | "SHOULD NOT be published" (RFC 7208, 5.5) |
exists: | 1 | Often used with macros, e.g. Salesforce |
ip4:, ip6:, all | ✓ 0 | — |
As of November 2026, per RFC 7208.
The second limit: two void lookups
RFC 7208 also says implementations SHOULD limit "void lookups", queries that return no record or a non-existent name, to two, and exceeding that limit also produces permerror. In practice this bites when you keep includes for services you cancelled: the vendor deletes the DNS name, and your record now points at nothing. An include whose target has no SPF record is a permerror on its own (Microsoft notes the same).
One record per name
A name may publish only one TXT record that starts with v=spf1. Two records, often the result of adding a new vendor as a second record instead of merging, also return permerror. Long records can be split into several quoted strings in one TXT record; receivers join them without adding spaces (RFC 7208, 3.3).
How do you count your SPF lookups?
Query your own TXT record with dig, count the six lookup terms in it, then query every include: and redirect= target and repeat until you reach records with only ip4/ip6. The total of all levels is the number receivers see.
# 1. Your own record (exactly one line must start with v=spf1)
dig +short TXT example.com
# 2. Every include: in it, then every include: inside those
dig +short TXT mailgun.org
dig +short TXT _spf.mailgun.org
Doing this by hand gets tedious after two levels, so here is a short Bash script that walks the tree and prints it. It uses only dig, sed and tr, and runs on Linux and macOS:
#!/usr/bin/env bash
# spf-count.sh: count the SPF terms that cost a DNS lookup, following include: and redirect=
# Usage: ./spf-count.sh example.com
spf() { dig +short TXT "$1" | sed 's/" "//g; s/"//g' | grep -i '^v=spf1'; }
count() {
local domain=$1 depth=$2 n=0 term lterm rec
rec=$(spf "$domain")
printf '%*s%s: %s\n' $((depth * 2)) '' "$domain" "${rec:-NO SPF RECORD}" >&2
for term in $rec; do
term=${term#[+~?-]}
lterm=$(printf '%s' "$term" | tr 'A-Z' 'a-z')
case $lterm in
include:*) n=$((n + 1 + $(count "${term#*:}" $((depth + 1))))) ;;
redirect=*) n=$((n + 1 + $(count "${term#*=}" $((depth + 1))))) ;;
a|a:*|a/*|mx|mx:*|mx/*|ptr|ptr:*|exists:*) n=$((n + 1)) ;;
esac
done
echo "$n"
}
total=$(count "$1" 0)
echo "Lookups: $total of 10"
chmod +x spf-count.sh
./spf-count.sh mailgun.org
Output, shortened:
mailgun.org: v=spf1 include:_spf.mailgun.org include:_spf.eu.mailgun.org -all
_spf.mailgun.org: v=spf1 include:_spf1.mailgun.org include:_spf2.mailgun.org ~all
_spf1.mailgun.org: v=spf1 ip4:159.135.224.0/20 ip4:69.72.32.0/20 ...
_spf2.mailgun.org: v=spf1 ip4:161.38.192.0/20 ip4:143.55.224.0/21 ...
_spf.eu.mailgun.org: v=spf1 ip4:141.193.32.0/23 ip4:159.135.140.80/29 ...
Lookups: 4 of 10
The total is what an include of that name costs below it; the include itself adds one more. So include:mailgun.org costs 5 lookups in your record. The script is deliberately simple: it does not expand macros (exists:%{i}… counts as one, which is correct) and it does not count the per-host A lookups behind mx.
How much does each provider's include cost?
We ran the script against the includes that providers document for their customers. The differences are large, and a single helpdesk can use more than half of your budget.
| Service | Include | Cost in your record (lookups) |
|---|---|---|
| Google Workspace | _spf.google.com | 1 |
| Microsoft 365 | spf.protection.outlook.com | 1 |
| Amazon SES | amazonses.com | 1 |
| Brevo | spf.brevo.com | 1 |
| IONOS (EU) | _spf-eu.ionos.com | 1 |
| SendGrid | sendgrid.net | 2 |
| Salesforce | _spf.salesforce.com | 2 |
| Zoho Mail (EU) | zohomail.eu | 2 |
| OVHcloud | mx.ovh.com | 3 |
| Mailgun | mailgun.org | 5 |
| Freshdesk | email.freshdesk.com | 7 |
As of November 2026: measured with the script above on 14 November 2026. Providers change their records without notice, so run it again before you rely on a number.
How do you fix "too many DNS lookups"?
Work from the cheapest change to the most invasive one: delete what is not needed, replace lookups that point at your own infrastructure with addresses, and move bulk senders to subdomains. Most domains get under 10 before flattening is needed.
Take this record, which is close to what many small companies end up with after a few years of adding tools:
; Before: 17 lookups, every message gets spf=permerror
example.com. 3600 IN TXT "v=spf1 a mx include:_spf.google.com include:mailgun.org include:email.freshdesk.com include:_spf.salesforce.com ~all"
That is 1 (a) + 1 (mx) + 1 (Google) + 5 (Mailgun) + 7 (Freshdesk) + 2 (Salesforce) = 17 lookups. Receivers that evaluate the whole record return permerror for every message from example.com, including ordinary staff mail.
- Check which senders actually use your domain in the Return-Path
SPF checks the domain in the envelope sender (MAIL FROM, visible as
Return-Pathin a delivered message), not the From address. Send a test from each tool to a Gmail account and open "Show original". If the Return-Path isbounces+123@vendor-mail.com, your record is never checked for that tool and its include is dead weight. Many helpdesk, CRM and newsletter tools work this way and pass DMARC through DKIM signed with your domain instead. - Remove includes for tools you no longer use
Cancelled services are the most common cause of both lookup overruns and void lookups. If nobody can say what an include is for, look it up with
dig; if its owner is a service you left, delete it. - Drop
ptr, and questionaandmxptris slow and RFC 7208 says not to publish it.aonly helps if the web server sends mail itself;mxonly if your inbound mail servers also send outbound. With Google Workspace or Microsoft 365, the MX hosts are already covered by the provider's include, somxis a wasted lookup. - Replace your own servers with
ip4:/ip6:For a server you control, such as an application server, a NetSendo MTA or an office relay, write its address directly:
ip4:203.0.113.10. It costs nothing and only changes when you change the server. - Move bulk and marketing senders to subdomains
Set up Mailgun on
mg.example.com, Amazon SES with a custom MAIL FROM such asbounce.example.com, and so on. Each subdomain gets its own SPF record and its own limit of 10. The next section explains why this still passes DMARC. - Flatten what is left, only if you must
If one vendor that must stay on the root domain still pushes you over, replace its include with its published address ranges and put a review date in your calendar.
After steps 1–5 the same domain looks like this:
; After: each name has its own budget of 10
example.com. 3600 IN TXT "v=spf1 include:_spf.google.com include:_spf.salesforce.com ~all"
mg.example.com. 3600 IN TXT "v=spf1 include:mailgun.org ~all"
bounce.example.com. 3600 IN TXT "v=spf1 include:amazonses.com ~all"
bounce.example.com. 3600 IN MX 10 feedback-smtp.eu-west-1.amazonses.com.
The root record now costs 3 lookups, mg.example.com costs 5 and bounce.example.com costs 1, each well inside its own limit. Freshdesk is gone from the root because, in this example, its Return-Path was the vendor's domain. The MX record on the SES subdomain is required by Amazon SES for a custom MAIL FROM domain, and the SPF record there is the one SES documents (Amazon SES: custom MAIL FROM domain). Use the region of your SES account in the MX host.
⚠️ Warning: Do not remove an include before you have confirmed, on a real message, where that tool's Return-Path points. Removing a sender whose envelope does use your root domain turns its SPF result into fail; it will only pass DMARC if it also signs with DKIM aligned to your domain.
Is SPF flattening a good idea?
Flattening replaces an include with the IP ranges it currently resolves to, so the sender costs no lookups. It works, but you take over a job the vendor was doing for you: noticing when their addresses change.
; Flattened by hand: 0 lookups for this sender, but the list goes stale when the vendor adds a range
example.com. 3600 IN TXT "v=spf1 include:_spf.google.com ip4:198.51.100.0/24 ip4:203.0.113.0/25 ~all"
Microsoft's guidance is a good rule of thumb. Flatten only vendors with stable, documented ranges, never Microsoft 365 itself, whose sending addresses change frequently, and if you do flatten, document what you replaced, watch vendor documentation, review the entries at least quarterly and test after each change (Microsoft Learn).
Static vs automated flattening
Static flattening is the example above: you copy the addresses once. It is free and transparent, and it breaks silently the day the vendor adds a range, because mail from the new range fails SPF while everything looks fine in your DNS panel. Automated flattening services re-resolve the includes on a schedule and publish the result, usually through an include or a CNAME to their own zone. That keeps the list current but adds a paid dependency in your authentication path; if the service fails, so does your SPF.
✅ Advantages
- Removes the lookups of the flattened vendor completely
- Faster evaluation, fewer DNS timeouts
- No change to how the vendor sends
❌ Considerations
- Stale addresses lead to SPF fails that nobody notices
- Long address lists push the record past the 450-octet guideline of RFC 7208, 3.4
- Automated services add a third party to your mail authentication
- Does not fix the root cause: too many senders on one name
Why does a sending subdomain avoid the problem?
Because SPF is evaluated for the exact domain in the envelope sender, and every name has its own record and its own budget of 10 lookups. Microsoft says it directly: "Each subdomain has its own 10 lookup budget", and it recommends subdomains for email services you don't control (Microsoft Learn).
DMARC is what makes this work for the visible From address. DMARC uses only the SPF result for the MAIL FROM identity, and that identity must be aligned with the From domain (RFC 9989, 4.4.2). Alignment is relaxed by default (aspf=r), so bounce.example.com and news@example.com share the organisational domain example.com and count as aligned. You can keep sending from news@example.com while SPF is checked on the subdomain.
It also matches what Gmail asks of every sender: SPF and DKIM set up for your domain, aligned at the organisational level (Google: Email sender guidelines). A separate subdomain for marketing has a second benefit: bounces and complaints from campaigns land on a name you can watch on its own. For a full walk-through of SPF, DKIM, DMARC and warm-up for your own servers, see our email deliverability guide for self-hosted senders.
💡 Tip: If your DMARC record setsaspf=s(strict), a subdomain envelope is not aligned and SPF stops counting for DMARC. Check your record withdig +short TXT _dmarc.example.combefore you move senders.
Should the record end in ~all or -all?
Once DMARC is in place, either works, because DMARC treats both ~all (softfail) and -all (fail) as SPF failures. Google recommends ~all (Google Workspace); Microsoft recommends -all for Microsoft 365 domains that also use DKIM and DMARC (Microsoft Learn).
The difference is what happens before DMARC runs. RFC 9989 warns that some receivers check SPF early in the SMTP session, so -all can get a message rejected even though it would have passed DMARC through an aligned DKIM signature, and such rejections never show up in your aggregate DMARC reports (RFC 9989, 7.1). This matters for forwarded mail and mailing lists. A practical rule:
- While you are still cleaning up includes and moving senders, use
~all. - For a subdomain used by one sender only, such as
mg.example.com,-allis low-risk: nothing else should send from it. - For the root domain with staff mail,
~allplus DMARC atp=quarantineorp=rejectgives you enforcement without early SPF rejections.
Neither choice fixes a permerror: with too many lookups, the all at the end is never reached.
How do you verify the fix?
Check the published record from outside your network, re-run the count, then send a real message and read the Authentication-Results header. Expect spf=pass for the envelope domain within the TTL of the old record.
# The record the world sees (ask a public resolver, not your cache)
dig +short TXT example.com @1.1.1.1
# Exactly one SPF record per name
dig +short TXT example.com @1.1.1.1 | grep -c 'v=spf1'
# The count after the change
./spf-count.sh example.com 2>/dev/null
Then send a message from each tool to a Gmail or Outlook.com mailbox and look at the headers. A broken record looks like this, with DKIM still saving the DMARC result:
Authentication-Results: mx.receiver.example;
spf=permerror smtp.mailfrom=news@example.com;
dkim=pass header.d=example.com;
dmarc=pass header.from=example.com
After the fix you want spf=pass and an smtp.mailfrom on the domain you planned. If you get the result dmarc=fail with SPF and DKIM both failing, see our guide to the Gmail 550-5.7.26 unauthenticated error.
📋 Checklist
- One
v=spf1record per name - 10 lookups or fewer, counted with nested includes
- No includes for cancelled services, no
ptr - Bulk senders on their own subdomains, each with its own record
- TTL of 3600 seconds, as Microsoft recommends, so changes spread within an hour
- DMARC aggregate reports show SPF passing for every source you know
Keep watching your DMARC aggregate (rua) reports for two weeks after the change: a source that suddenly shows SPF fail is usually a sender you did not know about, sending from the root domain.
SPF lookups when you send with NetSendo
NetSendo publishes no SPF include of its own, so it never adds a nested chain to your record. What your SPF needs depends on how a mailbox sends:
- NetSendo MTA (built into every installation since 2.11.0): the sending domain's record must list the server's public address, or the mailbox's dedicated IP, as
ip4:/ip6:. That costs no lookup. The panel shows the exact entry to add. - SMTP relays: NetSendo recognises Amazon SES, SendGrid, Google Workspace, Microsoft 365 and Mailgun by their SMTP host. For Amazon SES and SendGrid it adds no include, because those relays send with their own envelope domain (or the custom MAIL FROM you set up with them).
- Return-Path: when bounce handling is on for a mailbox, NetSendo sets the Return-Path to that mailbox's bounce address, so a bounce address on a subdomain moves the SPF check there. Setup is in the configuration docs.
On GOLD, the Deliverability module checks SPF, DKIM and DMARC for each domain, and its SPF generator keeps your existing record and only adds the includes your NetSendo mailboxes need. It shows a lookup count and flags records above 8. That count covers the terms in the record itself and does not expand nested includes, so for a record full of third-party includes, run the script from this article as well. To get started, follow the quickstart.
📌 Key takeaways
- The limit is 10 lookup terms across the whole tree, not 10 includes.
- Check Return-Paths before editing: many includes protect nothing.
- Subdomains give every bulk sender its own budget and still align for DMARC.
- Flatten only stable, documented ranges, and review them every quarter.
🎯 Expert tips
Write down, for every term in your SPF record, which tool it is for, who owns it and when it was added. The next permerror usually comes from an include nobody can explain.
Aim for 7 or 8 lookups, not 10. Vendors add nested includes without notice, and a record at exactly 10 can break overnight.
When marketing buys a new tool, decide on its subdomain before anyone pastes an include into the root record.
Drop the TTL to 300 seconds a day before you restructure the record, and raise it back to 3600 once the new record is confirmed.
Frequently asked questions
What does SPF PermError too many DNS lookups mean?
Evaluating your SPF record needed more than the 10 DNS-querying terms allowed by RFC 7208, so the receiver stopped and returned a permanent error. The message is treated as failing SPF, and DMARC cannot use SPF for it.
Which SPF mechanisms count toward the 10 DNS lookup limit?
include, a, mx, ptr and exists, plus the redirect modifier. Every include also counts all lookups nested inside it; ip4, ip6 and all do not count.
Does each subdomain have its own SPF lookup limit?
Yes. SPF is evaluated for the exact domain in the envelope sender, and every name has its own record and its own limit of 10. Microsoft recommends moving third-party senders to subdomains for this reason.
Is SPF flattening safe?
It works, but the copied IP addresses go stale when the vendor changes its ranges, and mail from new ranges then fails SPF. Flatten only vendors with stable, documented ranges and review the entries at least quarterly.
Does a permerror with too many lookups fail DMARC?
Only if DKIM fails too. DMARC passes when either SPF or DKIM passes and is aligned with the From domain, so an aligned DKIM signature still lets the message pass.
How many include statements can an SPF record have?
There is no fixed number of includes; the limit is 10 lookup terms across the whole evaluation. A single include that nests six more costs seven lookups.
Sources
- RFC 7208: Sender Policy Framework (SPF)
- RFC 9989: Domain-Based Message Authentication, Reporting, and Conformance (DMARC)
- Microsoft Learn: Set up SPF to identify valid email sources for your Microsoft 365 domain
- Google Workspace Admin Help: Set up SPF
- Google: Email sender guidelines
- Amazon SES: Using a custom MAIL FROM domain
Send from your own server with a short SPF record
NetSendo's built-in MTA needs one ip4: entry in your SPF record and no include chain.




