12 April 2026 · 12 min read
"Too many DNS lookups" is the most common way an SPF record breaks itself. The record is valid. The syntax is fine. Every include points at a real domain that publishes a real SPF record. And yet a receiver evaluating it gives up partway through and returns a permerror, because resolving the whole thing would take more than ten DNS queries. Under DMARC, that PermError counts as an SPF failure, so a record that looks healthy in a text editor can quietly cost you SPF alignment at the exact moment you move to enforcement.
This article is about that specific limit: where the budget of ten comes from, exactly which mechanisms spend it, how nested includes blow past it without you ever editing your record, and the two real ways back under the cap, consolidating and flattening, with the trade-offs of each spelled out honestly.
The error you are actually seeing
You will meet this problem in one of three places, and they look different enough that people do not realise they are the same issue.
In a checker or a header, as a permerror:
Authentication-Results: mx.google.com;
spf=permerror (google.com: too many DNS lookups while resolving
SPF record for example.com)
smtp.mailfrom=example.com;
dmarc=fail (p=reject dis=quarantine) header.from=example.com
In your aggregate (RUA) reports, as a band of rows where the SPF result is permerror and, if you are relying on SPF for alignment, the DMARC result is fail.
In a validator, as a blunt "10 lookup limit exceeded" or "too many DNS lookups (count: 12)".
All three are the same RFC 7208 rule. The receiver is not saying your record is malformed. It is saying that to finish evaluating it, it would have to issue more DNS queries than the specification permits, so it stops and declares a permanent error. The distinction matters because the fix is not "correct a typo", it is "make the record cost fewer lookups". For the difference between this and the transient cousin, temperror, see fix SPF TempError; this article is purely about the lookup-count PermError.
Where the number ten comes from
RFC 7208, the SPF specification, sets a hard ceiling: during evaluation of a single SPF record (following every include and redirect it leads to), a receiver MUST limit the number of DNS-querying mechanisms and modifiers to ten. Cross that, and the result is permerror. This is not a suggestion or a per-receiver quirk. It is mandatory, so Google, Microsoft, Yahoo and essentially every compliant receiver enforce it identically. There is no "more lenient mailbox provider" to fall back on.
The reason the limit exists is denial-of-service prevention. Without a cap, a malicious SPF record could chain include after include and force every receiver that checks it to perform an unbounded fan-out of DNS queries. Ten is the agreed ceiling that keeps SPF evaluation cheap and bounded. The full reasoning, with the spec citations, is in the 10-lookup limit glossary entry.
There is a second, separate cap that trips people who think they have solved the first one: the void lookup limit. RFC 7208 says a receiver SHOULD treat more than two lookups that return "no records" (an NXDOMAIN or an empty NODATA answer) as a PermError too. So even if your DNS-querying mechanisms total fewer than ten, a record littered with includes pointing at domains that no longer publish SPF can still PermError on void lookups. Stale includes are not free; they are actively dangerous.
Which mechanisms spend the budget, and which are free
This is where most people miscount. The ten-lookup budget is not "ten includes". It counts mechanisms and modifiers that require a DNS query, and crucially it counts them after expansion, because each include can drag in further lookups of its own.
Mechanisms that each cost one lookup:
include:(one per include term, plus whatever that included record itself costs)a(anda:domain)mx(one for the MX query, and be aware each returned MX host may need resolving)ptr(deprecated, and slow; remove it)exists:- the
redirect=modifier
Mechanisms that cost zero lookups, because they are already a literal address and need no resolution:
ip4:andip6:(a literal address or CIDR range)all(-all,~all,?all)- the
v=spf1version tag itself
That last list is the whole secret to fixing this error. ip4: and ip6: are free. Every lookup you can convert from an include into a set of literal IP ranges is a lookup you no longer spend. That is the mechanical basis of flattening, which we will get to.
The trap is the recursion. When you write include:_spf.google.com, that single line in your record is one lookup, but the included record itself contains more includes, and each of those counts against your ten. A common Google Workspace expansion looks like this:
_spf.google.com -> include:_netblocks.google.com
include:_netblocks2.google.com
include:_netblocks3.google.com
So include:_spf.google.com is not one lookup. It is the _spf.google.com query (1) plus the three nested netblock queries (3) = four lookups for a single line in your record. You wrote one include; you spent four. That is why people who "only have five includes" still hit the limit.
Counting your own record, by hand
Take a record that looks innocuous and count it properly. Suppose yours is:
v=spf1 include:_spf.google.com include:sendgrid.net include:servers.mcsv.net include:spf.protection.outlook.com include:_spf.salesforce.com mx ~all
Five includes, one mx, one ~all. Looks like six lookups, well under ten. Now expand each include as a receiver would (these expansions drift over time, so treat the numbers as illustrative of the principle, not as a fixed truth):
include:_spf.google.com-> 1 for itself + roughly 3 nested netblocks = 4include:sendgrid.net-> 1 for itself + its nested netblocks = about 3include:servers.mcsv.net(Mailchimp) -> about 1 to 2include:spf.protection.outlook.com(Microsoft 365) -> 1 for itself + nested = about 2 to 3include:_spf.salesforce.com-> about 1 to 2mx-> 1 for the MX query, plus a lookup per returned MX host if anya-style resolution is needed = 1 or more
Add the realistic figures and you are somewhere between twelve and fifteen, comfortably over ten. The receiver stops, returns permerror, and your SPF no longer passes. Nothing in your record is wrong; you have simply overspent. The fastest way to get the exact, current count for your domain is the SPF checker, which follows every nested include live and tells you the running total and where it tips over.
Why this bites you precisely at enforcement
While your DMARC policy is p=none, an SPF PermError is invisible in practice. Mail still gets delivered, nobody is enforcing anything, and the failure sits quietly in your reports. The moment you tighten to p=quarantine or p=reject, that same PermError becomes a real outcome.
Here is the chain. SPF PermError means SPF did not pass. DMARC needs at least one of SPF or DKIM to pass and align. If DKIM is also failing or unaligned on a given stream, DMARC has no passing mechanism, so DMARC fails, and at p=reject that message can be rejected outright. A record that "worked fine for months" breaks the week you flip to reject, not because anything changed, but because enforcement finally made the latent PermError matter. This is one of the reasons the move to enforcement should be gradual and report-driven rather than a single flip of a switch, as covered in reach p=reject without breaking email.
It also explains a frequent panic: "SPF passes when I test from my own server but fails in reports." Your manual test may resolve cleanly under no load, while a busy receiver hitting the lookup ceiling returns PermError. The record is on the edge, and the edge is non-deterministic in feel even though the lookup limit itself is deterministic.
Fix one: consolidate (always do this first)
Before you reach for flattening, prune. Consolidation is lower-risk and often enough on its own. Work through these in order.
1. Remove includes for senders you no longer use. This is the single biggest win and the most overlooked. Every former ESP, every trial of a marketing tool, every "we tested this for a quarter" service that is still in your record is spending lookups (and risking void lookups if the domain stopped publishing SPF). If you do not actively send through it, delete it. Pull a recent RUA file into the DMARC report analyzer to see which sources are genuinely sending, then remove anything that is not.
2. Delete ptr entirely. The ptr mechanism is deprecated by RFC 7208. It is slow, unreliable, and spends a lookup on a reverse-DNS query that frequently fails. There is no modern reason to keep it.
3. Drop mx if you do not send from your MX hosts. Many domains carry mx out of habit. SPF authorises sending IPs, and your inbound mail servers are often not the machines that send your outbound mail. If your sending is entirely through included providers, the mx lookup is pure waste. Remove it (and any a that exists for the same historical reason).
4. Use the provider's leanest include. Some providers publish more than one SPF macro, and the "everything" one is heavier than you need. If you only use one region or one product, check whether a narrower include exists. This is provider-specific, so confirm with their current documentation rather than guessing.
5. Subdomain delegation. You do not have to authorise every sender from one record on your root domain. If your marketing blasts go out as news.example.com and your transactional mail as example.com, give each its own SPF record. Splitting senders across subdomains gives each record its own fresh budget of ten, and it improves your reporting granularity into the bargain. See subdomain policy for how the DMARC side of that works.
After consolidating, recount with the SPF checker. Frequently this alone brings a bloated record back to six or seven lookups, which is the comfortable zone: under ten with margin to spare so a future provider expansion does not silently tip you back over.
Fix two: flatten (when consolidation is not enough)
If you genuinely send through enough live providers that even a pruned record breaks the limit, flattening is the tool. The idea is mechanical and follows directly from the "ip4: is free" rule above: you resolve an include chain down to the actual IP ranges it authorises, and publish those ranges as ip4:/ip6: literals instead of the include. Literals cost zero lookups, so a flattened record can authorise dozens of ranges while spending almost none of your budget.
A flattened fragment looks like this. Instead of:
v=spf1 include:_spf.example-esp.com ~all
you publish the resolved ranges directly:
v=spf1 ip4:198.51.100.0/24 ip4:203.0.113.0/25 ip6:2001:db8:a::/48 ~all
Same authorisation, zero lookups for that provider instead of three or four.
That sounds like a clean win, and mechanically it is. But flattening moves a cost rather than removing it, and you must understand the cost you are taking on.
You become responsible for the provider's IP changes. The reason include exists is so the provider can update their sending ranges and your record follows automatically. The moment you flatten, you have copied a snapshot of their IPs into your DNS. When they add a new sending range (they will, without telling you), your flattened record no longer authorises it, mail from that new range fails SPF, and you find out from a deliverability problem rather than a notification. Flattening trades a lookup-count problem for a maintenance problem.
Only flatten stable senders. Some providers explicitly tell you not to flatten them because their ranges change often. A large mailbox platform that rotates IPs frequently is a poor flattening candidate; a small fixed-IP relay is a fine one. The right move is usually to flatten the one or two stable, slow-moving providers that are eating the most lookups, and leave the volatile, well-behaved ones as include.
Re-resolve on a schedule, or automate it. A flattened record is only correct on the day you generated it. If you flatten by hand, you have signed up to periodically re-resolve every flattened include and diff the result against what you published. Miss that, and the record rots. The mechanics, and why automated re-flattening exists, are laid out in how SPF flattening works and the flattening doc, with the conceptual summary in the SPF flattening glossary entry.
The honest summary: flattening is the right answer when you have too many legitimate, active senders to fit under ten even after pruning, and it is the wrong answer if you reach for it before consolidating, or if you flatten a provider whose IPs move. Consolidate first; flatten only what consolidation cannot solve; and never flatten without a plan to keep it fresh.
A worked before-and-after
Start with the over-budget record from earlier:
v=spf1 include:_spf.google.com include:sendgrid.net include:servers.mcsv.net include:spf.protection.outlook.com include:_spf.salesforce.com mx ptr ~all
Audit against actual sending in the reports and find that Mailchimp (servers.mcsv.net) was a finished campaign and Salesforce mail now goes out as a subdomain. Apply consolidation:
- remove
ptr(deprecated, wasted lookup) - remove
mx(you do not send from your MX hosts) - remove
include:servers.mcsv.net(no longer sending) - move Salesforce to its own
news.example.comSPF record (fresh budget there)
That leaves three live includes:
v=spf1 include:_spf.google.com include:sendgrid.net include:spf.protection.outlook.com ~all
Recount. Google (about 4) plus SendGrid (about 3) plus Microsoft (about 2 to 3) is roughly nine to ten: legal, but no headroom, which means the next time any of those three providers adds a nested netblock you are back over the limit. So flatten the single most stable of the three. If SendGrid is your fixed-IP relay, resolve it and inline the ranges:
v=spf1 include:_spf.google.com ip4:198.51.100.0/24 ip4:203.0.113.0/25 include:spf.protection.outlook.com ~all
Now you are around six to seven lookups with genuine margin, and you have only taken on maintenance for one provider rather than three. Verify the rewrite with the SPF checker before publishing, then confirm the live record once DNS has propagated with the DMARC checker.
Things that do not fix it (and one that makes it worse)
- Splitting the record into two
v=spf1strings on the same name. Having two SPF records on one domain is itself a PermError, a worse one, because the receiver cannot decide which to use. One name, one SPF record, always. - Switching
-allto~all. Theallqualifier controls what happens to unlisted senders; it does nothing about the lookup count. A PermError is a PermError regardless of whether you end in softfail or hardfail. - Adding
?allor removingall. Same story. The error is about the cost of resolving the mechanisms beforeall, not aboutallitself. - Hoping a "friendlier" receiver ignores the limit. The ten-lookup cap is a MUST in the spec. The big providers enforce it. There is no receiver to escape to.
Confirm it, then keep it fixed
Because providers change their nested netblocks without warning, a record that passes today can quietly creep back over ten months from now. "I fixed it once" is not a stable state.
- Recount after every edit. Run the SPF checker and confirm your live lookup total has real headroom, ideally six or seven, not a knife-edge nine.
- Watch the reports. Open recent RUA data in the DMARC report analyzer and confirm the
permerrorband has gone, not just at one receiver but across them, since they all enforce the same limit. - Turn on change alerts. A lookup-count regression caused by someone else's provider expansion is exactly the failure that appears weeks after you last looked. Emailed monitoring with change alerts tells you when your SPF record or its resolved lookup count shifts, so you hear about it from an alert and not from a bounced one-time passcode. This is the same monitoring that underpins a safe path to enforcement, described in the requirements overview.
The takeaway
"Too many DNS lookups" is a budget problem, not a syntax problem. SPF allows ten DNS-querying mechanisms, counted after every include expands into its provider's nested includes, which is why a record with only five includes routinely spends twelve or fifteen lookups and PermErrors. ip4: and ip6: literals are free; include, a, mx, ptr and redirect are not. Fix it by consolidating first, removing dead senders, deleting ptr, dropping unneeded mx, and delegating senders to subdomains, then flatten only the stable providers that consolidation cannot fit under the cap, and only if you commit to keeping the flattened ranges fresh.
Start by getting your true lookup count from the SPF checker, prune what you no longer send through, and confirm the live record with the DMARC checker. If you would rather not hand-count nested netblocks or babysit a flattened record every time a provider rotates IPs, our hosted SPF keeps your record under the limit, flattens the senders that need it, re-resolves them automatically, and alerts you the moment your lookup budget moves, so "too many DNS lookups" stops being something you have to think about.