InboxGrade

SPF Void Lookup Limit exceeded (2+ NXDOMAIN responses)

More than two DNS lookups during SPF evaluation returned NXDOMAIN or an empty answer, which RFC 7208 Section 4.6.4 permits receivers to treat as `PermError`.

Impact — Medium impact

When evaluators enforce the void lookup limit, your SPF record resolves to PermError instead of pass, which usually results in a DMARC alignment failure and the message being quarantined or rejected. Even receivers that stop short of PermError typically downgrade the result and feed it into spam scoring, so delivery becomes inconsistent across Gmail, Microsoft 365, and secure gateways. Legitimate senders nested behind the dead include: keep failing silently until someone reads the Authentication-Results header or a DMARC aggregate report.

What causes it

  1. An include: or redirect= points at a vendor domain that was decommissioned or had its SPF TXT record deleted (for example a retired ESP, a sunset analytics product, or an acquired SaaS whose SPF subdomain no longer resolves).
  2. A typo in the SPF record such as include:_spf.gooogle.com or include:spf.protection.outlok.com causes every evaluation to hit NXDOMAIN.
  3. Nested include: chains reference a parent record that still exists but whose child mechanisms (a: or mx: of a dead hostname) return NXDOMAIN, and those count toward the void total.
  4. A ptr or exists: mechanism references a macro-expanded hostname that frequently does not resolve, producing void answers on most messages.
  5. The authoritative DNS for an include: target intermittently returns empty answers (REFUSED or SERVFAIL coerced to empty) due to a misconfigured resolver or split-horizon zone.

How to fix it

Enumerate every DNS lookup and flag the void responses

Walk the full SPF tree and record which mechanisms return NXDOMAIN or empty answers. dig +short on each include: and redirect= target is the fastest way to confirm; a void is any lookup that returns rcode 0 with no answers or rcode 3 (NXDOMAIN).

dig +short TXT _spf.example.com
dig +short TXT spf.retired-vendor.net
# Or use a parser that counts voids explicitly:
npx spf-check example.com --verbose

Remove or replace dead `include:` and `redirect=` targets

For each void mechanism, decide: remove it if the vendor is no longer sending for you, or replace it with the vendor's current SPF hostname. Do not leave a dead include: in place hoping it will come back — it is actively costing you one of your two allowed voids.

; Before
v=spf1 include:_spf.retired-vendor.net include:spf.protection.outlook.com -all

; After
v=spf1 include:spf.protection.outlook.com -all

Replace `ptr` and brittle `exists:` mechanisms

ptr is deprecated by RFC 7208 Section 5.5 and is a common void source because reverse zones are often incomplete. Remove ptr entirely. If you rely on exists: with macros, make sure the resolved hostnames actually exist in DNS, or scope the macro so it only triggers for authenticated senders.

Flatten vendor `include:` trees when you cannot control them

If a vendor's SPF record keeps changing and introducing voids, flatten their current ip4:/ip6: list into your own record and monitor it. This trades the void risk for a maintenance burden, so pair it with an automated check that re-flattens on a schedule and alerts on drift.

# Fetch and expand the vendor record, then paste the ip4/ip6 ranges into your own SPF
dig +short TXT spf.vendor.com | tr -d '"'

Fixed it? Run a fresh test to confirm.

Test my inbox →

Related errors