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

Terms evaluated inside an include share the same 10-term budget. Five top-level DNS-causing terms plus three nested includes give a structural total of 8, not 9. That total is not the result for every sender: SPF evaluates mechanisms in order and stops at a match (RFC 7208 §§4.6.2–4.6.4).

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.

Separate reference structure from sender evaluation

The following is a fictional, fixed DNS tree for teaching, not a provider DNS observation from October 2026. It avoids a recursive shell function that could loop on cyclic includes or disguise DNS failures as zero lookups.

example.com: v=spf1 ip4:203.0.113.10 mx a include:_spf.a.example include:_spf.b.example include:_spf.c.example ~all
_spf.a.example: v=spf1 include:_spf.a1.example include:_spf.a2.example include:_spf.a3.example ~all
_spf.a1.example: v=spf1 ip4:192.0.2.1 ~all
_spf.a2.example: v=spf1 ip4:192.0.2.2 ~all
_spf.a3.example: v=spf1 ip4:192.0.2.3 ~all
_spf.b.example: v=spf1 ip4:198.51.100.1 ~all
_spf.c.example: v=spf1 ip4:198.51.100.2 ~all

The top-level record has five DNS-causing terms: mx, a and three includes. The _spf.a.example record adds three includes; all other child records contain only ip4 and all. The reference-structure total is 5 + 3 = 8. It counts terms, not individual DNS packets or resolver cache misses.

For sender IP 203.0.113.10, the first ip4 mechanism matches, so evaluation stops with Pass before any DNS-causing mechanism: 0 evaluated DNS-causing terms. The initial TXT lookup to obtain the SPF policy is not one of these terms. Other sender IPs follow different paths and may match sooner or encounter a DNS error. redirect= is considered only after no mechanism matches, and is ignored when all is present (§6.1).

A structural count cannot determine Pass, Fail, TempError or PermError. Real evaluation also needs the sender IP and identity, DNS responses, macro expansion, the 10-term limit and additional MX/address and void-lookup limits. DNS failures, absent or multiple SPF records, cycles and timeouts must be reported as such, never as a successful count of zero. Use a maintained SPF evaluator for each real sending path; this guide is not an SPF implementation.

To inspect the published policy without the removed recursive function:

dig +time=2 +tries=1 TXT example.com

Check the command’s exit status and the DNS response status before reading the TXT answers. An empty answer is not a zero count. One SPF TXT record can contain several quoted strings, joined without inserting spaces; two separate SPF records are a different case and cause PermError.

The email check reports DNS-causing terms in the top-level record. It does not evaluate a sender IP or recursively resolve includes. Use it to inspect that record, and DNS lookup to inspect referenced policies; neither structural number guarantees a real SPF verdict.

Why some messages pass and others do not

Different senders can stop at different mechanisms. A sender matching the first IP range does not evaluate later includes; a sender matched later may first encounter lookup limits or DNS errors. Even a message with a forged From: address may come from an IP allowed by the envelope sender’s SPF, so forgery does not imply traversal of the entire tree.

Only an SPF Pass aligned with the From: domain can satisfy DMARC through SPF. A PermError cannot, but a valid aligned DKIM signature can still make DMARC pass. Receivers also apply their own acceptance policies. The rules are in RFC 7208 and RFC 7489 §3.1.

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. DMARC aggregate reports cover only reporting receivers and the periods received. Absence from a month of reports does not prove a service is unused. Cross-check the sending-system inventory, service logs and owner confirmation, including infrequent and seasonal mail. Remove an include only after confirming no active sending path needs it, then verify real messages and subsequent reports.

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. That single message is not enough to delete the include: confirm the service has no other active path using the apex MAIL FROM, including seasonal sends, before removing it and testing the remaining paths. 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

dig +time=2 +tries=1 TXT example.com

After checking DNS status and confirming exactly one SPF policy, evaluate each real sender IP and MAIL FROM with a maintained SPF evaluator. 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.