What the error means

permerror is SPF’s way of saying “your record could not be evaluated, so I’m not going to give you an answer”. “Too many DNS lookups” is the cause most people run into, and it is entirely self-inflicted: you, or the people who added senders before you, built a record whose evaluation needs more than 10 DNS queries.

The rule is in RFC 7208 §4.6.4. These terms cause DNS lookups and are capped at 10 in total for one SPF evaluation:

TermCounts?Notes
include:YesPlus everything inside the included record
redirect=YesPlus everything inside the target record
a, a:hostYesOne each
mx, mx:hostYesOne each; also fails if the domain has more than 10 MX hosts
ptrYesAnd the RFC says not to use it at all
exists:YesOne each
ip4:, ip6:NoNo DNS needed
allNo
exp=NoOnly looked up after a fail, to build the explanation

The part people miss is that the count is recursive. Your record may show five terms, but if one of them is an include that contains three more includes, you’ve spent nine lookups, not five. The limit applies to the whole tree.

There’s a second, lesser-known limit in the same section: void lookups. A lookup that returns no records (NODATA) or a name that doesn’t exist (NXDOMAIN) is a void lookup, and the RFC recommends that receivers allow at most two of them before returning permerror. An include: pointing at a vendor domain that no longer publishes SPF, or an a:old-mail.example.com whose A record was deleted, uses up that budget. An include whose target has no SPF record at all is a permerror on its own, regardless of the count.

Count your lookups properly

Start with the record as published:

$ dig +short TXT example.com | grep -i spf1
"v=spf1 mx a include:_spf.google.com include:mailgun.org include:sendgrid.net include:servers.mcsv.net ip4:203.0.113.10 ~all"

That looks like six lookup terms. To get the real number you have to follow every include and redirect. This shell function does it with dig and prints the path it took on stderr:

spfcount() {
  local rec t n=0
  rec=$(dig +short TXT "$1" | sed 's/" "//g; s/"//g' | grep -i '^v=spf1 ')
  for t in $rec; do
    t=$(printf '%s' "${t#[+~?-]}" | tr 'A-Z' 'a-z')
    case "$t" in
      include:*)  echo "$1 -> $t" >&2; n=$((n + 1 + $(spfcount "${t#include:}"))) ;;
      redirect=*) echo "$1 -> $t" >&2; n=$((n + 1 + $(spfcount "${t#redirect=}"))) ;;
      a|a:*|a/*|mx|mx:*|mx/*|ptr|ptr:*|exists:*) echo "$1 -> $t" >&2; n=$((n + 1)) ;;
    esac
  done
  echo "$n"
}

The sed matters: long TXT records are published as several quoted strings, and SPF joins them with no space in between. Run it against the example:

$ spfcount example.com
example.com -> mx
example.com -> a
example.com -> include:_spf.google.com
example.com -> include:mailgun.org
mailgun.org -> include:_spf.mailgun.org
_spf.mailgun.org -> include:_spf1.mailgun.org
_spf.mailgun.org -> include:_spf2.mailgun.org
mailgun.org -> include:_spf.eu.mailgun.org
example.com -> include:sendgrid.net
sendgrid.net -> include:ab.sendgrid.net
example.com -> include:servers.mcsv.net
11

Eleven. The vendor records in that trace are real as of October 2026: include:mailgun.org alone costs five lookups, include:sendgrid.net costs two, and Google’s and Mailchimp’s includes cost one each. Vendors restructure these records without telling anyone, which is how a record that was fine last year goes over the limit without anyone touching it. Run the count again rather than trusting a number from a forum post, including this one.

The function is deliberately simple. It doesn’t expand macros (%{i} and friends), and it doesn’t model the rule that redirect= is ignored when the record also has all. For ordinary records it gives the same answer as a full SPF library.

The email check reads your record and flags it when the lookup terms in the record itself already exceed 10. It doesn’t walk into includes, so a record it scores at seven can still be over the limit once the includes are expanded. Use it to see the record, and the function above for the full count. The DNS lookup shows each included record exactly as published, which helps when you want to see what a vendor’s include contains.

Why some messages pass and others don’t

Most receivers evaluate SPF left to right and stop at the first mechanism that matches. In the example above, mail from 203.0.113.10 is only matched by the ip4: term at the very end, so the receiver walks through every include first and hits the limit on the way. Mail from a Google server may match inside _spf.google.com after three lookups and pass.

So “SPF permerror” often shows up only for some senders: whichever ones sit at the end of the record, plus every forged message, since forged mail never matches and always walks the whole tree. Checkers that count the whole tree are showing you that worst case, and it’s the case that matters.

What happens next depends on the receiver. Under DMARC, an SPF permerror counts as an SPF failure, so a message with a valid DKIM signature aligned with your From: domain still passes DMARC. Mail that has no DKIM, or whose DKIM isn’t aligned, now has nothing to pass on. Some receivers also treat a broken SPF record as a negative signal in itself.

Getting back under 10

Work through these in order. The first three are free and usually enough.

1. Remove senders you no longer use

Every record accumulates includes for tools someone tried once. Go through the list and ask, for each one, who owns it and whether it sent anything last month. Your DMARC aggregate reports answer this directly: a service that doesn’t appear as a source in a month of reports isn’t sending as you. Remove its include.

Dead includes are worse than useless. If the vendor has retired that hostname, the include is now a void lookup or an include with no SPF record, either of which can produce permerror on its own.

2. Drop a, mx and ptr unless they really send

a authorises whatever your domain’s A record points at, usually your web server. If your website doesn’t send mail directly (contact forms usually send through your mail provider or an API), a is a wasted lookup.

mx authorises your inbound mail servers. With Google Workspace or Microsoft 365 those are the provider’s inbound hosts, and outbound mail is already covered by the provider’s include. mx only earns its place when your own server receives and sends mail from the same IP.

ptr is slow, unreliable, and RFC 7208 says it should not be used. Delete it.

3. Check whether each service needs to be in this record at all

This is the fix most people never find. SPF is checked against the envelope sender (the Return-Path, also called MAIL FROM), not the From: address the recipient sees. Many sending platforms use their own bounce domain, or have you point a subdomain such as bounce.example.com at them with a CNAME. For mail from those platforms, receivers check SPF on that bounce domain, and your apex record is never consulted.

Look at the headers of a message the service actually sent:

Return-Path: <bounces+81234-ab12@em5521.example.com>
Authentication-Results: mx.google.com;
       spf=pass (google.com: domain of bounces+81234-ab12@em5521.example.com designates 198.51.100.40 as permitted sender) smtp.mailfrom=bounces+81234-ab12@em5521.example.com;

The Return-Path domain is em5521.example.com, not example.com. An include for this service in the apex record does nothing for this mail and costs you lookups. Remove it, and check the result in the next round of DMARC reports. With DMARC’s default relaxed alignment, a bounce domain under example.com still counts as aligned.

4. Move senders to subdomains

The same idea used deliberately. If marketing, billing and support mail can come from news.example.com, billing.example.com and so on, each subdomain gets its own SPF record with its own 10-lookup budget. Users don’t notice the difference, and you get the side benefit that a reputation problem with one stream stays with that subdomain.

5. Flatten, but only with automation

Flattening replaces an include with the IP ranges it currently expands to:

v=spf1 include:_spf.google.com ip4:198.2.128.0/18 ip4:148.105.0.0/16 ip4:205.201.128.0/20 ~all

Each flattened include saves at least one lookup. The cost is that you now own a copy of someone else’s IP list. When the vendor adds a range, your record doesn’t know, and mail from the new range fails SPF. Nothing alerts you; you find out from DMARC reports or bounces.

If you flatten, do it with something that re-resolves the vendor includes on a schedule and updates your DNS: a script against your DNS provider’s API, or a commercial flattening service. Never hand-copy ranges once and walk away. Keep large vendors with frequently changing ranges as includes where you can, and flatten the small, stable ones.

Two practical limits while you’re at it. A single quoted TXT string can’t exceed 255 characters, so long records are split into several strings, which is fine because SPF joins them. And keep the whole record reasonably short; very large DNS answers may not fit in a UDP response and fall back to TCP, which is one more thing that can go wrong.

A variation of this is the only kind of “splitting” that helps: put flattened ranges into your own sub-records, such as include:_spf1.example.com, that contain nothing but ip4: and ip6: terms. Each costs one lookup and can hold many ranges. Splitting a record into several sub-records that still contain includes doesn’t reduce anything; it adds a lookup for each piece.

6. Macro-based SPF

There’s a further technique: exists:%{i}._spf.example.com. The %{i} macro expands to the connecting IP, and a DNS server you run answers with an A record if that IP is allowed. One lookup covers any number of senders. Several hosted SPF services work this way. It works, but your SPF now depends on that DNS service being up and correct, and the record is no longer readable by humans. It’s for organisations with many senders and someone who owns the result.

What not to do

  • Don’t publish two SPF records to “split the load”. Two TXT records starting with v=spf1 on the same name is a permerror in its own right (RFC 7208 §4.5).
  • Don’t switch to +all or remove -all/~all to make the error go away. +all authorises the entire internet to send as you.
  • Don’t fix it by deleting your main provider’s include. That trades a permerror for a plain fail on your most important mail.

Checking the fix

spfcount example.com                    # should print 10 or less
dig +short TXT example.com | grep -ci spf1   # should print 1

Then send a message to a Gmail address and open “Show original”: the SPF line should say PASS for the server that sent it. Repeat for each sending service you kept, because the one at the end of the record is the one that was failing. After the old record’s TTL has passed, the email check will show the new record and its lookup terms.

If SPF is fine but mail still fails DMARC, the problem is usually alignment or DKIM rather than SPF: see why DKIM fails and DMARC none vs quarantine vs reject. The email deliverability guide shows where SPF fits among the rest.

Check your own domain now

Free, no sign-up. Runs the exact check this guide describes and shows what to fix.