The SPF 10-lookup limit: diagnosis and flattening
Why SPF fails with permerror after ten DNS lookups, how to count them including nested includes, and safer alternatives to flattening your record.
What the rule says
SPF (RFC 7208) lets a domain list the servers allowed to send mail using it as the envelope sender. Evaluating a record can require DNS queries, and a record could chain includes endlessly, so §4.6.4 caps the work. Terms that cause DNS lookups are limited to 10 per evaluation, and exceeding the limit produces a permerror result.
| Term | Counts? | Notes |
|---|---|---|
include: | Yes | Plus every counting term inside the included record, recursively |
a, mx | Yes | mx also looks up the addresses of each MX host, at most 10 of them |
ptr | Yes | Deprecated by RFC 7208 §5.5; avoid it |
exists: | Yes | Often used with macros |
redirect= | Yes | Replaces the rest of the evaluation with another record |
ip4:, ip6:, all | No | No DNS query needed |
exp= | No | Only fetched to explain a failure, after evaluation |
The same section also limits void lookups: DNS queries that return no records or a name that does not exist. More than two of those in one evaluation is also a permerror, which catches includes that point at providers you no longer use.
The limit is per evaluation, not per record
What happens when you exceed it
A receiver that hits the limit stops evaluating and returns permerror. RFC 7208 treats that as a permanent problem with your record, and receivers generally treat it like a failure. SPF then fails for every message, including mail from servers that are correctly listed.
With a DMARC policy in place the damage depends on DKIM. Messages that carry an aligned DKIM signature still pass DMARC; messages that relied on SPF alignment fail and are quarantined or rejected under an enforcing policy. That is why a record that worked for years can break the day someone adds one more marketing tool.
Diagnosing your record
The Email Security resolves the record and every include and redirect from the XGM server. It shows the tree, counts lookups the way receivers do, flags void lookups and loops, and reports records that are close to the limit as a warning at eight or more. Use it before and after every change to the record.
You can also walk the tree by hand with dig. Query the root record, then query each include target and count again. It is tedious for large records, but it shows exactly where the lookups come from.
dig +short TXT example.com
"v=spf1 include:_spf.mail.example.net include:esp.example.net mx -all"
dig +short TXT _spf.mail.example.net
"v=spf1 include:_spf1.mail.example.net include:_spf2.mail.example.net ~all"| Finding | What to do |
|---|---|
n of 10 DNS lookups used (passed) | Nothing; keep room for future senders. |
n of 10 DNS lookups used (warning, 8 or more) | Plan a clean-up before the next vendor is added. |
n DNS lookups (limit is 10) (critical) | SPF is failing now; reduce lookups today. |
includes without an SPF record (void) | Remove includes for providers you no longer use. |
| Loop in the tree | An include points back to a record already evaluated; remove it. |
Auditing a record step by step
Take a record that has grown over the years for example.com. The SPF Checker reports 12 lookups and permerror, so every message relying on SPF currently fails. The table below lists each term with the lookups it costs, including nested ones, and what the audit found.
| Term | Lookups | Finding | Decision |
|---|---|---|---|
include:_spf.mail.example.net | 4 | Mailbox provider, sends most mail | Keep |
include:esp.example.net | 2 | Newsletter tool, DKIM already signs with example.com | Move to news.example.com |
include:oldcrm.example.net | 2 | CRM cancelled two years ago, no reports since | Remove |
include:helpdesk.example.net | 1 | Helpdesk, sends from its own bounce domain | Remove; DKIM covers DMARC |
mx | 1 | Inbound servers do not send outbound mail | Remove |
a | 1 | Web server sends contact form mail | Replace with ip4:192.0.2.80 |
ptr | 1 | Deprecated | Remove |
After the changes the root record uses four lookups, all from the mailbox provider, and the newsletter subdomain has its own record with two. The contact form keeps working through a fixed ip4 term. Nothing was flattened, so provider changes still flow through automatically.
example.com. IN TXT "v=spf1 include:_spf.mail.example.net ip4:192.0.2.80 -all"
news.example.com. IN TXT "v=spf1 include:esp.example.net -all"Before removing an include, confirm in your DMARC aggregate reports that the service either no longer sends or passes through aligned DKIM. The aggregate report guide shows how to read those rows.
SPF for HELO names and non-mail hosts
RFC 7208 also defines SPF checks for the HELO or EHLO name a server uses when it connects. Receivers use this identity when the envelope sender is empty, as in bounce messages. Publishing a small record for each mail server host name costs you nothing and keeps those checks passing.
mail.example.com. IN TXT "v=spf1 a -all"Host names that never send mail can publish v=spf1 -all. That includes web servers and other subdomains that appear in DNS but have no reason to be used as a sender. These records live on their own names, so they do not use any of the root domain's lookup budget.
Fixes that do not need flattening
1. Remove what you no longer use
Old records collect includes for tools that were cancelled years ago. Compare every include with your sender inventory and the sources in your DMARC aggregate reports. An include that never appears as a passing source for months is a candidate for removal.
2. Drop mx and a where they are not needed
mx authorises your inbound mail servers to send, which is only right if they actually send outbound mail. Many hosted mailbox providers deliver outbound mail from different servers covered by their own include. Removing an unnecessary mx or a saves a lookup each.
3. Let vendors use their own bounce domain
SPF is checked against the envelope sender (the return-path), not the visible From address. If a vendor sends with a bounce address on its own domain or on a subdomain like bounces.example.com, SPF is evaluated for that domain and your root record does not need the vendor's include at all. DMARC then passes through DKIM signed with your domain.
4. Split sending across subdomains
Each domain has its own SPF record and its own 10-lookup budget. Moving newsletters to news.example.com and receipts to billing.example.com moves their includes out of the root record. Each subdomain then needs its own DKIM setup and inherits the organizational DMARC policy.
; before: 11 lookups at the root
example.com. IN TXT "v=spf1 include:_spf.mail.example.net include:esp.example.net include:crm.example.net include:helpdesk.example.net mx -all"
; after: the mailbox provider stays at the root, other streams move
example.com. IN TXT "v=spf1 include:_spf.mail.example.net -all"
news.example.com. IN TXT "v=spf1 include:esp.example.net -all"
help.example.com. IN TXT "v=spf1 include:helpdesk.example.net -all"Flattening: how it works and why it is risky
Flattening replaces include mechanisms with the ip4 and ip6 ranges they currently resolve to. Address mechanisms do not count toward the limit, so a flattened record can list many providers with zero or one lookup. It solves the count, but it copies someone else's configuration into your DNS as a snapshot.
Providers change their sending ranges, sometimes without notice. A flattened record keeps the old ranges, and mail from the new servers fails SPF until someone updates the record by hand. The failure is silent: nothing in DNS tells you the provider's include changed.
| Aspect | include | Flattened ip4/ip6 |
|---|---|---|
| Lookups | 1 or more per provider | None for the flattened part |
| Follows provider changes | Automatically | Only when you update the record |
| Record size | Short | Long; may need several TXT strings |
| Failure mode | permerror when over the limit | Silent failures for new provider IPs |
| Maintenance | Low | Needs automation and monitoring |
If you flatten, automate it. A scheduled job should resolve the original includes, compare the ranges with the published record and alert or update when they differ. Keep includes for providers that change often, and flatten only stable ranges you control, such as your own servers.
Record size
example.com. IN TXT "v=spf1 ip4:192.0.2.0/24 ip4:198.51.100.0/24 ip4:203.0.113.0/24 " "ip6:2001:db8:10::/48 ip6:2001:db8:20::/48 include:_spf.mail.example.net -all"Macros and SPF hosting services
Some services avoid the limit with the exists mechanism and SPF macros. The record contains a term such as exists:%{i}._spf.service.example.net, and the service answers per sending IP whether it is allowed. The evaluation costs one lookup regardless of how many providers sit behind it.
This moves the list of your authorised servers into a third party's DNS. It can be a sound choice for large organisations, but understand the dependency: if the service is down or misconfigured, SPF fails for all your mail. Macros also make the record harder to read and audit, including for tools like the SPF Checker, which count the lookup but cannot see the list behind it.
Watching for changes you did not make
Includes are live references. When a provider reorganises its own SPF record, adding a regional include or a new partner range, your count changes the next time a receiver evaluates your record. You get no notification, and the first sign may be a DMARC report full of SPF failures.
A simple safeguard is a scheduled check of the lookup count. Run the SPF evaluation daily from a script or monitoring system and alert when the count reaches eight or when the result is not pass for a known sending IP. The XGM API returns the count as JSON, which makes it easy to wire into existing monitoring.
count=$(curl -s -H 'Accept: application/json' https://xgm.ro/api/v1/spf/example.com | python3 -c 'import sys, json; print(json.load(sys.stdin)["lookups"])')
if [ "$count" -ge 8 ]; then echo "SPF for example.com uses $count of 10 lookups"; fiKeep a copy of the full include tree from each check. When the count jumps, comparing two trees shows immediately which provider changed, and whether the change is permanent or a mistake on their side worth reporting.
Keeping it under the limit
- Add an SPF review to vendor onboarding: which include, how many lookups, and whether DKIM with your domain makes the include unnecessary.
- Run the Email Security after every DNS change and keep at least two lookups spare.
- Review DMARC aggregate reports quarterly and remove includes that no longer send.
- Prefer DKIM alignment for DMARC; SPF is a second layer, not the foundation.
- Document why each include exists, in a comment in your DNS repository or change log.
SPF also has a second guard that is easy to forget: only one SPF record is allowed per name. Two TXT records starting with v=spf1 produce permerror just like too many lookups, and they often appear when two teams add providers independently. The SPF guide covers the other common syntax errors.
FAQ
Does ip4 or ip6 count toward the 10 lookups?
No. Address mechanisms and all do not query DNS. Only include, a, mx, ptr, exists and the redirect modifier count, including those inside included records.
Is the limit 10 includes or 10 lookups?
Ten DNS-querying terms in total across the whole evaluation. One include whose record contains three more includes uses four.
Why did my record break without any change on my side?
A provider you include added nested includes to its own record, raising your total above ten. Includes follow provider changes automatically, which is usually good but can push you over the limit.
Should I use ~all or -all?
Both work with DMARC, which is what receivers enforce. -all states clearly that unlisted servers are not allowed; ~all is a softer signal often used while the list is incomplete.
Does exceeding the limit affect DKIM?
No, DKIM is evaluated independently. Messages with an aligned DKIM signature still pass DMARC even while SPF returns permerror.
Is flattening against the rules?
No, publishing ip4 and ip6 ranges is normal SPF. The risk is operational: the ranges go stale when providers change them, unless you update the record automatically.