Skip to content

A mail server that rejects your mail with no explanation and no bounce is usually not broken. It evaluated your SPF record, hit a hard limit, got permerror, and treated the domain as unauthenticated. The limit is explicit in the specification, and most SPF records in the wild that "suddenly stopped working" crossed it by adding one more include.

The number, and which terms count

RFC 7208 section 4.6.4, "DNS Lookup Limits," is unambiguous. Six terms cause DNS queries at evaluation time:

  • include
  • a
  • mx
  • ptr
  • exists
  • redirect

Implementations MUST limit the total number of those terms to 10 during evaluation. Exceed it and the implementation MUST return permerror. The terms all, ip4, ip6 and the exp modifier do not count, because they resolve nothing.

Two details make this worse than it looks. First, the limit is global across the whole evaluation, not per nested include — the RFC says the limit "must be tracked as a single global limit for all evaluations." So an include that itself contains include chains eats the same budget. Second, the mx mechanism counts MX record queries toward the same 10, so a domain with many MX hosts burns the budget without you writing a single extra include.

Why it presents as "no SPF"

Receivers treat permerror as a permanent error and, in practice, as an absent policy. A message with no usable SPF still usually gets delivered when DMARC is in none mode — so the failure is invisible until DMARC moves to quarantine or reject, at which point everything from that domain starts landing in spam or bouncing. The gap between the trigger and the symptom is often months, which is why the cause gets misattributed.

There is a second trap in the same section: void lookups. A void lookup is a DNS query that returns no answer. The RFC says implementations SHOULD limit void lookups to two. So even under the 10-term ceiling, a couple of includes pointing at domains that no longer publish records can independently sink evaluation.

How to count before you break it

Do not count by eye. Every serious SPF tooling does the walk for you and reports nested lookups, and the number it prints is the one that matters. When you audit a record, the questions are:

  1. How many include terms are there at the top level?
  2. How many does each of those resolve internally, recursively?
  3. Does any mx term pull a large MX set into the budget?
  4. Does the record use ptr at all? Almost nothing should — RFC 7208 section 5.5 formally deprecates it.

The fix is subtraction

Three moves, in order of impact:

  • Drop ptr. Deprecated, slow, and it counts against the budget.
  • Flatten. Replace an include that only exists to add a handful of IPs with explicit ip4:/ip6: terms. Those cost zero lookups. The trade-off is maintenance: flattened records stop tracking the provider's IP changes, so flatten only stable ranges.
  • Consolidate senders. Every marketing platform, CRM, and ticketing system wants its own include. Each one is a permanent tax on the same 10-term budget. A record with seven vendor includes has 3 lookups left for everything else, forever.

What to do when it is already broken

If mail is currently failing, do not start by rewriting all. Change the record so the count drops below the ceiling while keeping the existing pass/fail behaviour for legitimate senders, verify with the tooling, then tighten policy. Reordering mechanisms so cheap ones match first also helps: SPF is evaluated left to right and stops at the first match, so ip4 terms placed early resolve the common case without spending lookups.

The takeaway

10 DNS-lookup terms, tracked globally, with mx included and void lookups capped at two. Cross it and SPF reports permerror, which reads as "no policy" and usually fails silently until DMARC enforcement arrives. Count before adding, flatten what is stable, and treat every new vendor include as a permanent subtraction from a fixed budget.

Keep reading