Guides

Fix an SPF PermError from too many DNS lookups

18 October 2026

An SPF PermError means the published record could not be evaluated. One cause, in RFC 7208 section 4.6.4, is crossing ten DNS-querying terms during a single check. The usual store version is a domain that has collected an include for every app that ever sent mail.

What the limit actually counts

SPF implementations must limit the following terms to 10 during the whole evaluation. Past that, the result is permerror:

all, ip4, and ip6 do not query DNS when the record is evaluated, so they do not count toward the ten. The exp modifier looks up an explanation string later, and it is also outside this limit.

The counter is global. Section 4.1 says the limit covers every recursive evaluation, not ten per record. An include that itself contains three include mechanisms can spend four of your ten before your own record has finished.

Two neighboring limits in the same section are easy to mix up with the ten-term cap:

ptr counts as a lookup, and RFC 7208 discourages it. Adding ptr does not relieve the limit.

How to audit nested includes

Start at the one v=spf1 TXT on the domain that appears in MAIL FROM. Walk the mechanisms from left to right and write down every term from the list above. Each include: opens that name’s SPF record and continues the same tally.

A receiver stops at the first mechanism that matches. A static audit still has to assume the expensive path: the sender that is authorized only after several includes have been evaluated. Count the terms a check must evaluate to authorize that sender, including the terms inside each included record. If that path is over ten, mail from that sender can come back permerror even though an earlier sender in the record looked fine.

redirect counts too, but only if evaluation reaches it. If the record contains an all mechanism, RFC 7208 says the redirect is ignored. You cannot “add both” and expect the redirect to save an over-long record.

ip4 and ip6 inside those included records authorize hosts without adding to the tally. They are why a provider’s include can list many networks and still cost you one lookup at your layer.

Remove senders you no longer use

An include for an ESP you cancelled still costs a lookup once evaluation reaches it. Remove mechanisms for services that do not send. Confirm the list with whoever runs the stack (the store admin, the ESP, the person who added a review app) before you delete a term. A missing include makes that mail fail SPF.

After the cut, recount. The goal is one record whose most expensive legitimate path stays at or under ten DNS-querying terms.

One SPF record, and only one

Section 3.2 says a name must not have multiple records that would cause a check to select more than one. Section 4.5 is the consequence: after discarding TXT records that do not begin with exactly v=spf1, more than one remaining record is permerror.

Other TXT records at the same name are allowed. A second string that also starts with v=spf1 is not. People hit this by adding “one SPF record per vendor” in the DNS panel. Merge them into a single policy:

v=spf1 include:mail.example.net include:send.example.org ~all

One DNS TXT record can be stored as several character-strings. Those strings are concatenated with nothing inserted between them, and they are still one record. Two separate TXT answers that both start with v=spf1 are the broken case.

The risks of flattening

Flattening means replacing an include, a, or mx with the ip4 and ip6 addresses those names resolve to today. The lookup count drops, because those mechanisms do not query DNS at evaluation time. The cost shows up later.

Flatten after you have removed senders you do not use, and only if the remaining includes still exceed ten. Prefer one current include per provider you actually send through.

Soft fail (~all) and -all

The qualifier on all is the result for hosts that matched nothing earlier. - is fail. ~ is softfail. ? is neutral. A missing qualifier means pass, which is why a bare all at the end would pass everyone.

Softfail, in section 2.6.5, is a weak statement that the host is probably not authorized. The domain has not published the stronger fail policy. Section 8.5 tells receivers not to reject a message solely because of softfail. They may scrutinize it more closely.

Fail (-all) is the definitive “not authorized” result. RFC 7208’s introduction says that if you want receivers to make a negative determination, the record needs to end in -all, or redirect to a record that does. Section 10 is there because that determination has sharp edges, including mailing lists that rewrite MAIL FROM.

DMARC does not treat the two as different kinds of success. RFC 7489 counts only an SPF pass, and that pass has to align with the From domain. Softfail and fail both fail to produce that pass. Choosing ~all or -all changes the SPF result receivers see on their own. It does not decide whether unauthorized mail passes DMARC.

The practical trade-off: ~all while you are still finding every sender, so a missed include is a weak signal. -all once the record lists every host that should pass and you want a hard fail for everyone else. Switching to -all before that list is complete fails mail you still send.

Sources