SPF Too Many Lookups: The Limit That Breaks Client Mail Without Warning.
SPF allows ten DNS lookups. Cross it and authentication fails for the whole domain. We counted the real number on seven live domains, including ours.
Short answer: an SPF record may trigger at most ten DNS lookups when a receiving mail server evaluates it. Go over and the result is a permerror, which most receivers treat as a failure. Your client’s mail starts landing in spam and nothing in WordPress, their DNS panel, or their mail client says a word about why.
The spf too many lookups problem is nasty because you cross the line with a change that looks completely routine. Somebody adds a newsletter tool. That is one more include:. The record still looks tidy. Authentication is now broken for every message the domain sends.
Counting it properly on seven real domains
The count is not the number of things listed in the record. It is the number of DNS queries a receiver has to make, and includes nest.

Our own sitea.example sits at seven, with five includes, one of which nests another inside it. Three lookups of headroom, and no warning anywhere as that shrinks.*
Salesforce and Shopify are in there for scale. Two of the largest senders on the internet run at four. If your client’s record is at nine, that is a choice somebody made by accident.
Re-counted, and the drift this post warns about already happened
I re-read and recursively re-counted all six of our own SPF records before expanding this post, rather than reuse the original chart. Two held steady exactly. Two grew.
dig +short TXT sitea.example | grep spf1
# then resolve every include: recursively, the way a receiver does
sitea.example still counts to 7: five includes (zeptomail.net, a spam-filtering provider, _spf.google.com, _spf.elasticemail.com, sendgrid.net), one exists: nested inside the Elastic Email include, one further include: nested inside SendGrid’s own record. Same total as the original reading, even though the specific vendor list has changed underneath it. siteb.example also held exactly at 4: a, mx, and two clean includes with nothing nested inside either.
sited.example did not hold. The original record was v=spf1 include:_spf.google.com ~all, one lookup, described in this post’s own table as “correct, one sender, minimal.” The current record is v=spf1 a mx include:_spf.elasticemail.com include:_spf.google.com ~all:
a -> 1
mx -> 1
include:_spf.elasticemail.com -> 1, plus its own exists: -> +1
include:_spf.google.com -> 1
total: 5
Five lookups, up from one, a fivefold increase, still comfortably under the limit of ten but no longer the minimal record this post held up as the example to copy. sitec.example moved similarly, from 2 lookups to 4, after gaining the same Elastic Email include with its nested exists: mechanism.
Neither change looks alarming in isolation, five and four are still well inside the ten-lookup budget, which is exactly the point worth making rather than skipping past: this is what the gradual creep this post describes actually looks like while it is still safe. Nobody added six lookups in one afternoon. Two separate additions, each reasonable on its own, each added by someone who was not counting, moved two real domains a third and a half of the way to a limit neither addition was evaluated against. The next tool added to either domain inherits whatever headroom is left, not a fresh budget, and nobody currently has a note anywhere saying what that headroom is.
What actually costs a lookup
This is where the counting goes wrong, because several mechanisms cost a lookup without looking like they should.

+a +mx is two lookups spent before you have listed a single sending service. I haveseen records where removing those two, on a domain whose mail is entirely handled by a provider anyway, brought the count back under the limit on its own.*
| Mechanism | Costs | Notes |
|---|---|---|
include: | 1, plus its own | nested includes count too; one can cost four |
redirect= | 1, plus its own | same as include for counting purposes |
a | 1 | easy to overlook, costs the same as an include |
mx | 1 | +a +mx is two lookups before a single sender is listed |
exists: | 1 | used inside some providers’ includes, spent on your behalf |
ptr | 1 | deprecated, remove it |
ip4: / ip6: | 0 | free, no DNS query needed |
-all / ~all | 0 | free |
Note ip4: is free. That is the escape hatch.
When a record is over the limit, the fix is nearly always to replace an include with the IP ranges it resolves to. You are trading a DNS lookup for a maintenance obligation, so do that for infrastructure you control, and leave the third-party includes alone. A provider that changes IPs without telling you will break the record you hardcoded.
How to count SPF lookups yourself, too many or not
There is no way to see this in a DNS panel, because the panel shows you one record and the cost is spread across records you do not own.
dig +short TXT example.com | tr -d '"' | grep -i "v=spf1"
Read the record, then resolve each include: and count what those contain, recursively. Any a, mx, exists or ptr adds one. Stop at ten.
If you would rather not do it by hand, that recursion is about thirty lines of script, and it is the same logic a receiving server runs on every message.
Two things worth checking while you are there:
- More than one SPF record on the same domain is an instant
permerror, regardless of
lookup count. It happens when two people each add one. There must be exactly one.
~allversus-all. Softfail asks receivers to accept but mark. Hardfail asks them to
reject. Neither matters much until DMARC is enforcing, at which point it matters a lot.
The posture most agencies never set
Here is the one I care about most, and it takes two minutes per domain.

from is a free identity for anyone wanting to impersonate your client. v=spf1 -all with no MX closes it and costs nothing. The last row is one of ours, and it is on the list.*
| Domain | SPF record | Posture |
|---|---|---|
sitee.example | v=spf1 -all | correct, sends no mail, rejects anything claiming to be |
sited.example | v=spf1 include:_spf.google.com ~all | correct, one sender, minimal |
sitec.example | v=spf1 include:zeptomail.net include:_spf.google.com -all | correct, two senders, hard fail |
sitef.example | none published | gap, DMARC set to quarantine with no SPF to align against |
Caveat: four domains, all ours, chosen because I could verify every claim against records I control. This is not a survey of how agencies configure SPF generally. It is what checking our own portfolio actually found, including the one row that needed fixing.*
Every agency has these. The old domain from a rebrand. The .net bought defensively. The campaign microsite from two years ago. None of them send mail, all of them can be spoofed, and publishing a single TXT record ends it.
The one gap this post already flagged is still open
I checked sitef.example again too, the row this post’s own table already marked as a gap: DMARC set to quarantine with no SPF record to align against.
dig +short TXT sitef.example | grep spf1 # empty
dig +short TXT _dmarc.sitef.example
v=DMARC1; p=quarantine; rua=mailto:[email protected]; fo=1
Still no SPF record at all, and the DMARC policy is still actively enforcing quarantine against an alignment mechanism that does not exist for this domain. That is worth sitting with alongside the two domains that quietly grew: one gap named in public, in a published post, sits unaddressed, while two unrelated domains picked up new SPF lookups nobody was watching for. Visibility and action are not the same thing, and a finding written down does not fix itself just because it has been documented.
Why it stays broken
Because the failure is entirely on the receiving side.
Nothing bounces. The mail leaves. The client’s outbox looks normal. The only symptom is that a gradually larger share of their mail lands in spam, which reads as a marketing problem for about a quarter before anyone checks DNS. By then the client has usually paid somebody to rewrite subject lines.
It is worth pairing this check with the sending side of the same question. An SPF record can be perfect while the site never actually dispatches the message, because WordPress hands a lot of its mail to the scheduler, and a stopped scheduler sends nothing while looking completely healthy. From the client’s chair those two failures are indistinguishable.
That is the same silence as the scheduled jobs that stop and the plugin that left the directory: no error, no alert, no obvious moment when it changed.
Protuno’s free audit counts the SPF lookups from the domain alone, alongside the DMARC policy and the MX records, so you can see the whole portfolio’s mail posture in one place. Straight with you as ever: Echo, the Marketing agent that would re-check this as records drift, is built and named but not live yet.
Run the dig above on the client domain you have added a tool to most recently. That is the one that moved.
What tracking the count over time would actually catch
The re-count above is the argument for treating this as a number worth watching on a schedule rather than a one-time audit. sited.example going from one lookup to five is not a single event that happened once and stopped; it is two separate additions, a and mx mechanisms presumably added when the domain’s own hosting or mail routing changed, and the Elastic Email include added for a specific sending need. Each one, checked in isolation at the time, looked like a reasonable, self-contained change. Neither person adding either mechanism had the prior count in front of them, because nothing surfaces it automatically.
A record tracked over time, the same six lookups counted monthly and written down alongside the date, would have shown the jump from one to five as a visible, explainable step rather than a number nobody was watching. That is a different kind of check from the audit this post already describes: not “is this domain currently over the limit,” which a single reading answers, but “how much headroom is left, and is it shrinking,” which only a series of readings compared against each other can show. The domains most likely to actually hit the ten-lookup ceiling are not the ones sitting at nine today. They are the ones climbing steadily, unnoticed, the way sited.example just did, where the person adding lookup number eleven has no idea they are the one who finally breaks it.
Recounting after every new tool, not only during an audit
The practical process worth adopting from this re-check: whenever a client or an internal team adds a new email-sending tool, a marketing platform, a transactional email provider, a CRM that sends on the domain’s behalf, the SPF lookup count for that domain should be recounted as part of adding the tool, not discovered later during an unrelated audit. That is a two-minute check against the loop already given in this post, run at the exact moment a new include: is about to be added, when the person making the change is already looking at the DNS record and is best positioned to notice if the addition pushes the domain close to the limit.
Waiting for a quarterly or annual audit to catch this means the domain can spend months over the limit, silently failing SPF on every message, before anyone reads the record closely enough to notice. The fix costs nothing extra when it happens at the moment of change. It costs a quarter of misdelivered mail when it is discovered later, which is the same arithmetic this blog keeps returning to for every other check that is cheap in the moment and expensive once deferred.
Comments