dotvitals

Fix SPF PermError: too many DNS lookups

Updated ·10 min read

SPF evaluation is capped at ten DNS-querying terms. Cross that ceiling and the check does not simply become less accurate — it returns a permanent error, and the domain gets no benefit from SPF at all. Mail that would have passed now does not, and because the failure is in the record rather than in any particular message, it applies to every message you send.

The limit is in RFC 7208 §4.6.4, and it is a hard requirement on the receiver, not a recommendation: "If this number is exceeded during a check, a PermError MUST be returned." This guide covers what counts against the ceiling, how to count your own record honestly including everything nested inside your providers' records, and the four ways out, ordered by how much you will regret each one later.

Check yours now

What the error looks like when you meet it

You will usually see it in one of three places. In a received message's Authentication-Results header it appears as spf=permerror. In a DMARC aggregate report it appears as an SPF result of permerror across every sending source at once, which is the giveaway — a problem with one sender affects one sender, a problem with the record affects all of them. And in an SPF checker it appears as a lookup count above ten.

The practical consequence is that SPF no longer contributes a pass to DMARC. If your domain also has working DKIM, DMARC still passes on the DKIM side and you may never notice; if DKIM is missing or broken for some sending path, DMARC now fails for that path, and under an enforcing policy that mail is quarantined or rejected. This is why an SPF permerror sometimes surfaces as "mail from one specific tool stopped arriving" rather than as an obvious global failure.

What counts, and what does not

Six terms cause DNS queries and count against the limit of ten: the include, a, mx, ptr and exists mechanisms, and the redirect modifier. Three do not: ip4, ip6 and all, because they are answered from the text of the record itself.

The cost is transitive. An include does not cost one lookup; it costs one lookup plus everything inside the record it pulls in. A provider whose published record contains two includes of its own charges you three, and you cannot see that from your own record — you have to go and look.

Two details are easy to miss. The a and mx mechanisms cost one lookup each no matter how many addresses come back, but resolving the hosts an mx mechanism returns has its own separate cap of ten names. And there is a second, smaller budget: RFC 7208 §4.6.4 also limits "void lookups" — queries that return no answer or a non-existent name — to two. A record referencing an include for a service that has been shut down burns void lookups and can fail on that limit while still being under ten overall.

One consequence of how evaluation works surprises people: mechanisms are evaluated left to right and stop at the first match, and the limit applies to the check rather than to the record. A record that is over budget can therefore pass for a sender matched by an early ip4 mechanism and permerror for a sender that would have matched an include near the end. An intermittent-looking SPF failure that correlates with which tool sent the message is usually this.

Counting a real record

github.com is a useful worked example because it is a large organisation's record sitting exactly at the ceiling. Read as of 2026-09-13, its record has eight includes and no a or mx mechanisms.

Expanding each of those eight: spf.protection.outlook.com, _netblocks.google.com, _netblocks2.google.com, mail.zendesk.com, servers.mcsv.net and mktomail.com each contain only ip4 and ip6 mechanisms, so they cost one lookup each and nothing more. sendgrid.net contains include:ab.sendgrid.net, so it costs two. _spf.salesforce.com contains an exists mechanism, which is a DNS query, so it costs two as well.

That is 6 + 2 + 2 = 10. Exactly at the limit, with no margin: adding a single further include — one more marketing tool, one more helpdesk — would push this record into permerror. Our SPF checker warns before you get there rather than only at the moment it breaks, and the number to aim for is seven or lower, which leaves room for a provider to add a nested include to their own record without breaking yours.

dig +short github.com TXT — observed 2026-09-13, reassembled from its two strings
v=spf1 ip4:192.30.252.0/22 include:spf.protection.outlook.com
  include:_netblocks.google.com include:_netblocks2.google.com
  include:mail.zendesk.com include:_spf.salesforce.com
  include:servers.mcsv.net include:mktomail.com include:sendgrid.net
  ip4:62.253.227.114 ip4:166.78.69.169 ip4:166.78.69.170
  ip4:166.78.71.131 ~all

Counting your own, by hand

The checker does this for you, but doing it once by hand is worth the ten minutes, because it makes the shape of the problem obvious and tells you which branch is expensive.

Fetch your own record, then fetch the record for every domain named in an include or redirect, then repeat for anything they reference in turn. Count one for every include, a, mx, ptr, exists and redirect you encounter anywhere in the tree.

Keep the per-provider subtotals rather than only the grand total. The decision you are about to make is which provider to change, and that is a question about branches, not about the sum.

Walk the tree one level at a time
# your own record
dig +short example.com TXT | tr -d '"' | grep spf1

# every domain it includes, one level down
for d in spf.protection.outlook.com _spf.google.com sendgrid.net; do
  printf '%s: ' "$d"
  dig +short "$d" TXT | tr -d '"' | grep spf1
done

Fix 1: remove what no longer sends

Start here every time, because it is the only fix with no downside. Most over-budget records are over budget by accumulation: a marketing platform trialled two years ago, a helpdesk that was replaced, a payment provider that now sends from its own domain.

Work out which of your includes correspond to services that still send mail as your domain. DMARC aggregate reports answer this directly — they list every source that sent mail claiming to be you, with volumes — which is one of several reasons to have a reporting address in place before you start editing SPF.

Remove an include only when you are confident nothing behind it still sends. If you are unsure, this is recoverable: putting it back is a one-line edit that takes effect within the record's TTL, and until then mail from that source softfails rather than disappearing, assuming your record ends in ~all.

Fix 2: give a bulk sender its own subdomain

This is the fix that should be better known, because it solves the problem structurally rather than trading one cost for another. The lookup budget is per SPF check, and an SPF check is against one domain. Move a sender onto its own subdomain and that subdomain gets a fresh budget of ten.

Concretely: configure the platform to use news.example.com as its envelope sender domain — most bulk and transactional senders support this and many recommend it — and publish an SPF record at news.example.com containing only that platform's include. The apex record loses an include and the subdomain has nine lookups of headroom.

Two things to get right. The subdomain needs its own DKIM setup at the platform, because DKIM keys are published per domain. And DMARC alignment still works: relaxed alignment, which is the default, treats news.example.com as aligned with a From address at example.com, so nothing about your DMARC policy needs to change. Use strict alignment and it would not, which is one of the few concrete reasons to leave adkim and aspf at their relaxed defaults.

Splitting one bulk sender off the apex record
; before — apex carries everything
example.com.      3600 IN TXT "v=spf1 include:_spf.google.com include:sendgrid.net include:servers.mcsv.net ~all"

; after — apex keeps only what it needs
example.com.      3600 IN TXT "v=spf1 include:_spf.google.com ~all"
news.example.com. 3600 IN TXT "v=spf1 include:servers.mcsv.net ~all"
mail.example.com. 3600 IN TXT "v=spf1 include:sendgrid.net ~all"

Fix 3: consolidate senders

If three separate tools each send a few hundred messages a month and each costs you a lookup, the cheapest lasting fix is often to stop using three tools. This is an organisational change rather than a DNS change, and it is slower than the others, but it is the only one that reduces the number of places your domain's reputation can be damaged from.

It is worth raising explicitly when the record is over budget because a department signed up for something independently. The SPF limit is frequently the first technical control that makes shadow sending visible, and the conversation it forces is usually more valuable than the lookup it saves.

Fix 4: flattening, and what it actually costs

Flattening means replacing include:provider.example with the ip4 and ip6 ranges that provider's record currently contains. It works, it is immediate, and it reduces the lookup count to zero for that provider. It is also the fix to reach for last, and it is worth being precise about why rather than simply calling it bad practice.

The include mechanism exists so that a provider can change its sending infrastructure without every customer editing DNS. Flattening opts you out of that. When the provider adds a network range — which they do, without announcing it to you, because the include was the announcement — your record no longer authorises it, and mail from the new range starts failing SPF. There is no error and no notification. You find out from a deliverability complaint or from DMARC reports, usually weeks later.

If you flatten anyway, do it deliberately: flatten only providers who publish their ranges as a documented, stable list; record somewhere durable which includes you replaced and when; and re-check the provider's published record on a schedule rather than treating the edit as finished. Undoing it is easy — put the include back and remove the ranges — so the risk is not that it is irreversible, only that you will not know when to reverse it.

Hosted "SPF flattening" services automate the re-checking by publishing a record you point at with an include or redirect. That is a real answer to the maintenance problem, and it is also a new dependency: your domain's ability to send mail now depends on a third party's DNS being correct and available. That may well be a trade you want to make; make it knowingly rather than because a tool offered it.

Verifying the fix

Re-run the SPF checker against the domain once the new record's TTL has passed, and confirm three things rather than one: that the lookup count is at or below the target, that every sender you still use is covered by a mechanism, and that the record still ends in a single all mechanism.

The second of those is the one that gets skipped, and it is where the damage happens. A record that is comfortably under the limit because you removed an include a live service was relying on is a worse outcome than the permerror you started with — permerror at least fails symmetrically.

Then watch DMARC aggregate reports for a week. They will show whether real mail from each source is now passing SPF in alignment, which is the only evidence that actually settles it. A checker tells you the record parses; the reports tell you it is correct.

What commonly goes wrong

  • Counting only your own record's mechanisms and ignoring what is nested inside each include. This is the single most common reason a record that "only has five includes" is over budget.
  • Publishing a second SPF record instead of merging into the existing one. Two records at the same name is a permerror in its own right, independent of lookups, and it is what most provider setup wizards will produce if you let them.
  • Using the ptr mechanism to save a lookup. It still costs one, it is slow, and RFC 7208 §5.5 says it SHOULD NOT be used; our checker flags it.
  • Replacing an include with a /8 or similar very large range copied from a support article. A /8 authorises sixteen million addresses, most of which are not your provider's; our checker grades an overly broad range as a finding, and a range of /0 through /7 the same way it grades +all.
  • Tightening to -all in the same edit as the lookup fix. Change one thing at a time: get the record under the limit, confirm from reports that every source passes, and only then move from ~all to -all.

Check your domain with the spf checker