12 June 2026 · The DMARC Engine Team · 12 min read
When email authentication breaks, it rarely breaks loudly. A misconfigured SPF record does not throw an error in your sending dashboard, it does not bounce your newsletters back to you, and it does not light up a red warning in your DNS panel. Instead it fails quietly, in the receiving server's evaluation step, in a way you only discover when a customer says your invoice landed in their spam folder or when your DMARC reports start filling with spf=permerror. This article is about one specific and very common cause of that silent failure: the SPF 10 DNS-lookup limit, the PermError it produces, and why crossing it can make your carefully built SPF record stop protecting you altogether.
What SPF actually does, in one paragraph
Sender Policy Framework is a published list of who is allowed to send email using your domain in the envelope MAIL FROM address. You publish it as a single TXT record on your domain, starting with v=spf1. When a receiving mail server gets a message claiming to be from you@yourdomain.com, it looks up that TXT record, walks through the mechanisms you listed, and decides whether the connecting IP address is authorised. The record ends with an all mechanism that says what to do with everything you did not explicitly permit. That is the whole idea: a public allow-list of sending sources, evaluated in real time by the receiver.
The catch is that SPF was deliberately designed to be cheap for receivers to evaluate. A receiver should not have to perform an unbounded number of DNS queries just to check one incoming message, because that would turn SPF into a denial-of-service amplifier. So the specification, RFC 7208, puts a hard ceiling on how much DNS work a single SPF evaluation may cost. That ceiling is where most real-world SPF breakage comes from.
The 10 DNS-lookup limit
RFC 7208 section 4.6.4 states that the number of mechanisms and modifiers that cause a DNS lookup must not exceed 10 during a single SPF check. If evaluation requires more than 10, the receiver must return PermError.
The mechanisms that count against this budget are the ones that have to resolve something in DNS:
include(each one costs at least 1, and triggers a full nested evaluation)amxptr(count it, but never use it, more on that below)exists- the
redirectmodifier
The mechanisms that do not cost a lookup are the ones that need no DNS resolution:
ip4ip6all
This distinction is the single most important thing to understand about SPF performance. ip4:198.51.100.25 is free. include:_spf.example.com is expensive, and it is expensive in a way that compounds.
A crucial subtlety: the limit is not "10 entries in your record". It is 10 lookups across the entire recursive evaluation, including everything reached through your includes and their includes. Your own record might list only four include mechanisms and look perfectly tidy, while the real cost, once every nested include is expanded, is 14 or 18 or 23 lookups. You cannot see that cost by reading your own TXT record. You have to follow the whole tree.
There is a second, separate limit that catches people out: inside any single mx mechanism, no more than 10 MX records may be evaluated, and inside any single ptr mechanism, no more than 10 PTR records. These are sub-limits, distinct from the global count of 10 lookups.
The void-lookup limit
There is a less famous limit that produces the same fatal result. RFC 7208 also caps the number of void lookups: DNS queries that return either an empty answer (NOERROR with no records) or NXDOMAIN (the name does not exist). The limit is 2. If more than two of your lookups come back void, the receiver returns PermError.
This one bites when an include in your chain points at a hostname that has been retired, or when a third party deprecates an SPF sub-record and the name stops resolving. You did nothing wrong in your own record. A vendor quietly removed a DNS name, two of your nested lookups now go void, and your SPF flips to PermError for every message. Void lookups are a strong argument for monitoring your record continuously rather than checking it once at setup and forgetting it.
What PermError means, and why it makes SPF fail open
PermError is a permanent error. It tells the receiver "this SPF record is broken in a way that re-trying will not fix". It is distinct from TempError, which is a transient DNS failure where the receiver may try again later.
Here is the part that surprises people. When SPF returns PermError, the message is not rejected. SPF has no defined hard-fail behaviour for PermError. The receiver treats the result as "I could not determine an SPF answer", and in almost all real deployments that means the SPF check is effectively skipped. Your -all at the end of the record, the part that was supposed to say "reject everything else", never gets reached, because evaluation aborted before it got there.
This is what people mean when they say PermError makes SPF fail open. The protection you thought you had, the explicit "only these sources are legitimate" statement, simply does not apply. Anyone can send mail claiming to be your domain and SPF will not stop them, because your SPF evaluation never completes. The record is still published, it still looks present and correct in a casual glance, and it is doing nothing.
It gets worse when DMARC is involved. DMARC passes when either SPF or DKIM passes and aligns. If your SPF is throwing PermError, you have lost one of your two authentication legs. You are now relying entirely on DKIM to keep DMARC passing. If a particular sending source signs with DKIM and aligns, you might never notice. But any source that relied on SPF for alignment, and there are usually several, will start failing DMARC. Depending on your policy, that is messages going to spam or being rejected outright. The first symptom is often "some of our transactional email stopped arriving" rather than any error you can see at the sending end.
You can confirm whether this is happening to you with a DMARC checker and by reading the aggregate reports: PermError shows up explicitly in the SPF result field of RUA data. If you are not yet collecting those reports, our monitoring collects and parses them for you and alerts you when an authentication result changes.
How nested includes blow the budget
The 10-lookup limit feels generous until you see how quickly mainstream providers consume it. Every include you add is not one lookup, it is one lookup plus whatever that provider's record costs when fully expanded, recursively.
Consider a fairly ordinary mid-sized business that uses Google Workspace for staff mail, a marketing platform for campaigns, a CRM for sales sequences, a help-desk tool for support, and a transactional API for receipts. Their record might look like this:
v=spf1 include:_spf.google.com include:spf.protection.outlook.com
include:servers.mcsv.net include:sendgrid.net include:_spf.salesforce.com
-all
Five includes. Reading it, you might count five lookups and feel safe. But each of those names expands:
include:_spf.google.comitself contains three further includes (_netblocks,_netblocks2,_netblocks3), so it costs 1 plus 3, totalling 4.include:spf.protection.outlook.comadds 1.include:servers.mcsv.netadds 1.include:sendgrid.netadds 1, and historically expanded into further includes.include:_spf.salesforce.comadds 1, sometimes more.
You are already at roughly 8 lookups from five visible includes, and that is before any of the nested providers adds an extra layer. Add one more vendor, or let one provider grow their own record by a single nested include (which they can do at any time, without telling you), and you are over 10. The day they do it, your SPF starts returning PermError. Nothing changed on your side. This is why SPF breakage so often appears "out of nowhere".
The deeper problem is that the cost is owned by other people. When you add include:somevendor.com, you are handing that vendor a blank cheque drawn on your 10-lookup budget. If they restructure their record next quarter and it now expands to four lookups instead of two, your budget shrinks and you may not find out until DMARC reports show the failure.
How to count your lookups
You cannot eyeball this from your own TXT record, because the cost lives inside the nested records. You have to expand the whole tree. There are three practical ways.
1. Use a checker that expands recursively. The fastest route is our SPF checker, which resolves your record, follows every include, redirect, a, mx and exists, counts the total lookups, flags void lookups, and tells you exactly how close to the limit you are and which branch is the most expensive. This is the right tool to reach for first because it does the recursion correctly, including the sub-limits.
2. Walk it by hand with dig. If you want to understand your own record deeply, expand it manually. Start with your record:
dig +short TXT yourdomain.com
Then for every include, look up that name's TXT record and repeat for any includes you find inside it:
dig +short TXT _spf.google.com
dig +short TXT _netblocks.google.com
Keep a running tally. Count one for each include, a, mx, ptr, exists and redirect you encounter at any level. Count zero for every ip4, ip6 and all. Note any lookup that returns nothing, those are your void lookups, and you get only two before PermError. It is tedious, and the tedium is exactly why this limit catches so many people: nobody does this by hand more than once. You can also inspect the raw TXT data for any name with our TXT record checker or DNS record checker if you want to see the unparsed answer.
3. Monitor it continuously. Counting once tells you the state today. It says nothing about tomorrow, when a vendor restructures their record or a name goes void. Continuous monitoring is the only honest answer to a limit whose cost is partly controlled by third parties.
How SPF flattening fixes it safely
The structural fix for the lookup-limit problem is SPF flattening. The idea is simple: since ip4 and ip6 mechanisms cost zero lookups, you replace expensive include mechanisms with the actual IP ranges they ultimately resolve to.
Instead of:
v=spf1 include:_spf.google.com include:sendgrid.net -all
a flattened record contains the resolved address ranges directly:
v=spf1 ip4:209.85.128.0/17 ip4:64.233.160.0/19 ip4:66.102.0.0/20
ip4:167.89.0.0/17 ... -all
Now your record has zero DNS lookups beyond the one to fetch the record itself. You can list dozens of providers without ever approaching the limit, because IP mechanisms are free. The 10-lookup ceiling stops being a constraint.
Flattening has one serious catch, and it is the reason you must not treat it as a one-time edit. The IP ranges behind a provider's include are not yours. They change. Google, Microsoft, and every large email platform add and retire netblocks regularly, and they do it precisely so they can manage their infrastructure without asking every customer to update DNS. When you flatten, you snapshot their IP ranges at one moment in time. If they add a range next month and you have hard-coded the old list, mail from their new servers will fail SPF, the exact opposite of what you wanted.
So flattening done safely means flattening done dynamically: something must continuously re-resolve every provider's record, detect when their IP ranges change, and update your flattened record automatically, without you editing DNS by hand. Flattening without that automation trades one silent failure (PermError) for a different silent failure (legitimate mail rejected because a provider's new IPs are not in your snapshot). Both are bad. The difference is that PermError fails open and the stale-snapshot problem fails closed, which can be worse because it actively blocks your own mail.
This is the core of what our hosted SPF product does: it flattens your record to stay comfortably under the limit, then keeps the flattened result current by re-resolving your sources on a schedule and updating automatically when a provider changes their netblocks. You publish one record once and the maintenance happens for you. If you would rather hand the whole authentication stack over, the done-for-you service manages SPF, DKIM, DMARC, MTA-STS and BIMI together and takes the domain from p=none to p=reject without an email outage.
Other ways to reduce lookups without flattening
Flattening is the heavy-duty fix, but there are lighter changes worth making first, because a tidy record is easier to reason about.
Remove sources you no longer use. Old records accumulate includes for tools you stopped using two years ago. Each dead include is wasted budget, and if its name ever goes void it becomes a PermError waiting to happen. Audit the list and delete anything that no longer sends mail for you.
Replace a and mx with ip4 where the addresses are stable. If your mail server's IP rarely changes, ip4:198.51.100.10 is both free and explicit, where a and mx each cost a lookup. Only do this where you genuinely control the address and it is stable.
Never use ptr. The ptr mechanism is slow, unreliable, costs a lookup, and is explicitly discouraged by RFC 7208. Receivers may ignore it or treat it harshly. If you see ptr in a record, remove it.
Use subdomains to split sending streams. Bulk marketing mail does not have to share a domain, and therefore an SPF record, with your transactional and corporate mail. Sending campaigns from mail.yourdomain.com gives that stream its own SPF record with its own 10-lookup budget, separate from the budget your primary domain spends on staff mail and receipts. This also improves reputation isolation: a marketing deliverability problem stays off your primary domain.
The role of ip4, ip6 and the all qualifier
Two parts of the record deserve a closer look because they are where you exert the most direct control.
ip4 and ip6 are your free, authoritative statements. They name exact addresses or CIDR ranges that are permitted, they require no further DNS resolution, and they are unambiguous. Flattening works precisely because these mechanisms are free. The trade-off is that they are static: an IP you list is permitted until you remove it, so if you list a range you later stop controlling, you have left a door open. Use them for sources whose addresses you know and can maintain, and let automation maintain the ones that change.
The all qualifier is the verdict on everything else. It sits at the end of the record and decides the fate of any sender you did not explicitly authorise. The qualifier in front of it matters enormously:
-allis hard fail: anything not listed is unauthorised. This is the goal. Combined with DMARC, it is what actually stops spoofing.~allis soft fail: anything not listed is suspicious but not rejected. Useful as a transitional state while you confirm you have catalogued every legitimate sender, but it is not a finished posture.?allis neutral: it asserts nothing, which is barely better than having no policy at all.+allmeans pass everything, which authorises the entire internet to send as your domain. Never publish this.
The order of evaluation matters too. SPF reads left to right and stops at the first match. Because all matches everything, it must always be last. Any mechanism placed after all is dead and will never be evaluated. A common mistake is appending a new ip4 after -all and wondering why the new server still fails: it was never reached.
Note that PermError changes the meaning of your all qualifier in practice. You can publish -all with full conviction, but if your record throws PermError the receiver never reaches -all at all. A correct hard-fail policy and a lookup count under 10 are not two separate goals, they are the same goal. The -all is only as strong as your ability to evaluate the record that precedes it.
Putting it together: a sane SPF posture
A healthy SPF setup, in order of priority:
- One published
TXTrecord per sending domain, startingv=spf1, ending in-all. Never publish two SPF records on the same name, that itself is a PermError. - Under 10 DNS lookups across the full recursive expansion, with margin to spare so a vendor's change does not tip you over.
- Zero or one void lookups, never approaching the limit of two.
- Explicit
ip4/ip6for stable sources, includes only where a provider's ranges genuinely change. - Dynamic flattening if your legitimate source list cannot fit under 10 lookups any other way, with automation that keeps the flattened result current.
- Continuous monitoring so a third party's change, or a name going void, alerts you instead of silently degrading your authentication.
SPF fails in silence. There is no bounce, no console error, no red banner. The only way you learn that your record crossed the lookup limit is by reading DMARC reports that show permerror, by a deliverability complaint, or by checking the record before it breaks. Run your domain through the SPF checker to see your real lookup count and find the most expensive branch, build or repair the record with the SPF generator, and browse the full set at /tools. If you would rather not babysit a number that other companies can change without warning, our hosted SPF flattening keeps you safely under the limit and current as your providers shift their ranges, so your -all actually means what it says.