SPF “too many DNS lookups”: the 10-lookup limit and how to fix it
Why SPF fails with PermError: too many DNS lookups, how the 10-lookup limit is counted, and how to fix it. Paste your SPF record to count its lookups.
7 min readDNSMigrator team
On this page
Five include terms can exceed SPF's ten-lookup limit. Each included policy can contain more include, a, mx, exists, or redirect terms, and the receiver counts them across the recursive evaluation. Count the dependency tree, not the text visible in your root TXT record; flatten only when you can keep the resulting addresses current.
The counter shows the root policy's lookup terms immediately. It cannot know the complete total until every included policy is resolved, and those dependencies can change. Use it to find obvious pressure, then inspect the recursive graph as described below or use the SPF record builder with a resolver-backed check.
Why SPF says “too many DNS lookups”#
RFC 7208 section 4.6.4 requires an SPF implementation to limit these DNS-triggering terms to ten during one evaluation:
includeamxptrexists- the
redirectmodifier
Exceeding ten produces permerror. The count is global across recursive includes and redirects. Qualifiers do not change the cost: include:x, ~include:x, and -include:x each contribute a term when evaluated.
ip4, ip6, all, and the exp modifier do not count toward that ten-term limit. DNS caching may make repeated questions cheaper for a receiver, but it does not erase terms from the SPF processing limit.
There are separate limits inside mechanisms. Each mx evaluation may query no more than ten address records for its MX hosts. ptr has its own address-query handling and is explicitly discouraged by RFC 7208 section 5.5. The RFC also recommends a default limit of two “void lookups”—queries that return NXDOMAIN or a successful response with no answers. A third can produce permerror even when the ten-term count is lower.
How SPF PermError from too many DNS lookups happens#
Consider this root record:
example.com. 300 IN TXT "v=spf1 include:_spf.mail-a.example include:_spf.mail-b.example include:_spf.crm.example mx -all"It has four lookup terms at the top level: three includes and one mx. Now inspect the included policies:
_spf.mail-a.example. 300 IN TXT "v=spf1 include:_spf.shared.example a -all"
_spf.mail-b.example. 300 IN TXT "v=spf1 include:_spf.shared.example a mx -all"
_spf.crm.example. 300 IN TXT "v=spf1 a exists:%{i}._spf.crm.example -all"
_spf.shared.example. 300 IN TXT "v=spf1 ip4:192.0.2.0/24 ip6:2001:db8::/48 -all"The count is:
root: 3 include + 1 mx = 4
mail-a: 1 include + 1 a = 2
mail-b: 1 include + 1 a + 1 mx = 3
crm: 1 a + 1 exists = 2
shared: ip4 and ip6 = 0
--
total 11The repeated include of _spf.shared.example counts on both evaluation paths even though a receiver may cache its TXT answer. The policy is over the limit before considering whether the exists query becomes a void lookup.
Fetch the published root without losing the RRset context:
dig +noall +answer example.com TXTA real-shaped answer is:
example.com. 300 IN TXT "v=spf1 include:_spf.mail-a.example include:_spf.mail-b.example include:_spf.crm.example mx -all"Then query every include and redirect recursively. dig displays DNS; it does not evaluate SPF control flow or stop when an earlier mechanism matches. A worst-case static count asks how many lookup terms a receiver may encounter before reaching a result.
One SPF policy means one selected TXT record#
SPF version 1 is published in TXT, not the obsolete SPF RR type. RFC 7208 requires a receiver to return permerror if more than one record at the owner name begins with v=spf1.
Do not solve length or team ownership by publishing this:
example.com. 300 IN TXT "v=spf1 include:_spf.marketing.example ~all"
example.com. 300 IN TXT "v=spf1 include:_spf.corporate.example -all"Those are two policies, not two halves of one policy.
A single TXT RR can contain multiple character-strings. DNS concatenates them without adding spaces, as RFC 7208 section 3.3 explains:
example.com. 300 IN TXT (
"v=spf1 include:_spf.marketing.example "
"include:_spf.corporate.example -all"
)That solves the 255-octet character-string encoding boundary without adding an SPF lookup. It does not reduce the lookup terms already in the policy.
DNSMigrator's translation engine converts an obsolete SPF-type record to TXT with an informational note. Its SPF parser also rejects multiple selected SPF values when resolving an include. Use zone lint and validation to catch the policy shape during a move.
What DNSMigrator counts#
The SPF builder parses mechanisms, follows include and redirect TXT policies when a resolver is available, detects include loops, and recursively sums lookup terms. It warns when the result exceeds the constant limit of ten. If nested records were not resolved, its warning explicitly says nested includes were not counted.
That count covers the RFC lookup terms represented by the parser and any include chain created by the builder's overflow split. It is not a full live SPF evaluator. In particular, the current implementation does not model the separate two-void-lookup recommendation or enumerate the A/AAAA queries under each mx. Treat the builder warning as a strong structural check, not proof that every possible sender evaluation succeeds.
A DNSControl-style builder declaration can keep the policy visible as parts:
SPF_BUILDER({
label: "@",
parts: [
"v=spf1",
"include:_spf.mail-a.example",
"include:_spf.mail-b.example",
"ip4:192.0.2.0/24",
"-all"
],
flatten: ["_spf.mail-a.example"]
})When flattening is requested and a resolver is present, DNSMigrator resolves the selected include, substitutes its mechanisms recursively, and removes the included policy's terminal all. By default the builder also writes the original policy at _rawspf, giving the generated policy a source record. If no resolver is available, it warns and leaves the include unflattened.
When SPF flattening helps—and what it changes#
Flattening helps only when substitution turns lookup mechanisms into lower-cost terms, usually ip4 and ip6. Replacing one include with a child policy that contains three more includes may make the root longer without reducing the recursive count.
More importantly, an include delegates maintenance to another domain. If a mail vendor adds or removes sending ranges, every receiver sees its updated include after DNS caches expire. A flattened copy does not. You now own a materialized snapshot and need an automated refresh, review, publication, and rollback path. A stale flatten can reject legitimate senders or continue authorizing addresses the vendor retired.
Flattening is a reasonable fix when all of these are true:
- The dependency pushes the policy near or over the limit.
- The resolved policy primarily produces stable IP ranges.
- The vendor permits and documents this use, or you control the included domain.
- An automated job refreshes the flattened result before upstream changes matter.
- Changes are diffed and tested rather than published blindly.
- The unflattened policy is retained for audit and recovery.
It is usually the wrong first fix when you can remove an abandoned sender, replace your own a or mx with known ip4/ip6 ranges, or consolidate duplicate services. A redirect can centralize policy across domains, but it still costs a lookup and does not create an extra budget.
Splitting can spend another lookup#
DNSMigrator's builder has an overflow mode that can move terms into additional TXT records and link them with generated include terms. Each link is part of SPF evaluation, so the builder adds the chain length to its lookup count.
Use that only when you need separate owner names for a policy that cannot fit provider limits. Do not confuse it with splitting one TXT RR into DNS character-strings, which costs no SPF lookup. RFC 7208 section 3.4 also recommends keeping the complete SPF response small enough for broad DNS transport compatibility; merely making each string legal does not guarantee a compact response.
How to fix SPF too many DNS lookups#
- Inventory every service that legitimately sends with the domain's envelope-from or HELO identity.
- Remove includes for retired services.
- Expand the full include and redirect graph; detect loops and duplicate paths.
- Count
include,a,mx,ptr,exists, andredirectglobally. - Test for void lookups and inspect MX fan-out separately.
- Replace DNS mechanisms you control with documented IP ranges when maintainability allows.
- Flatten only selected external dependencies and automate their refresh.
- Publish exactly one
v=spf1policy at each owner name. - Query the authoritative nameservers and send test mail through every legitimate path.
- Inspect
Authentication-Resultsat receivers rather than assuming a TXT query proves evaluation.
A DNS migration is a good time to run this audit because every TXT RR is already under review. Preserve the policy first; optimize it only after you can account for every sender and every lookup. Include SPF in the email-safe DNS migration checks, compare old and new TXT RRsets with the DNS record diff tool, and track the change in the DNS migration checklist. After cutover, use the DNS migration checker to confirm the intended authority is live, then inspect real Authentication-Results headers.
What does SPF PermError: too many DNS lookups mean?
include, a, mx, ptr, exists, and redirect at ten across recursive evaluation.How do I fix SPF too many DNS lookups?
ip4 or ip6 ranges where maintainable. Flatten only selected dependencies and automate refresh, testing, and rollback.Do nested SPF includes count toward the 10-lookup limit?
include and redirect policies. DNS caching may reduce network work, but it does not reset the SPF processing count.What is the SPF void lookup limit?
permerror even when the policy has not exceeded ten DNS-querying terms.