Start with the mechanism, not the score

RFC 7208 recommends limiting “void lookups” to two; these are lookup attempts that return no usable answer, including certain empty-answer and Name Error responses. Exceeding the configured limit can produce PermError.

A void lookup is not simply “a DNS query that failed.” In SPF evaluation it is a lookup that returns a valid negative result such as NXDOMAIN or an answer with no relevant records. That distinction matters because a resolver timeout or SERVFAIL belongs to a different failure class. The RFC places a separate limit on these empty-result lookups so a malformed or adversarial policy cannot make receivers chase an endless chain of names that resolve to nothing. When auditing a sender, annotate which SPF term produced each empty answer. Two different dead include targets may reveal stale vendor configuration, while the same target appearing through multiple branches may reveal a more subtle nested-policy problem.

Where operators commonly misclassify the result

Operators often see “7 of 10 lookups” and conclude SPF has comfortable headroom. That number says nothing about dead branches that repeatedly resolve to NXDOMAIN or to an answer with zero records.

The common mistake is to focus only on the headline “10 lookups” rule and ignore empty-result behavior. A policy can remain under the overall DNS-querying term budget yet still be fragile because several include, exists, or redirect paths point at names that return no usable data. Another mistake is to treat every negative DNS response as proof that the provider is gone. A temporary delegation problem, split-horizon DNS, or query for the wrong record type can look empty from one resolver. Before removing anything, identify the exact mechanism and confirm the negative answer against authoritative DNS or another independent resolver.

How to reproduce it without adding volume

Expand the policy with a resolver that shows result codes, not only flattened text. Mark each include, a, mx, exists or redirect that ends in NXDOMAIN, NODATA, timeout, or a valid record and compare the actual route used by the sending IP.

Reproduce the problem with the same domain and record type an SPF evaluator will request. Save the TXT policy, then walk the mechanisms in order and log both the DNS return code and whether the requested RRset exists. For nested includes, repeat the process inside the child policy rather than counting only the root. If a checker reports PermError, compare its trace to your own resolver output; the useful part is the sequence of names, not the badge color. A live test message can then confirm whether a receiver actually records spf=permerror for the affected path. That gives you protocol evidence before you change production DNS.

A narrow remediation path

Remove references to retired providers, repair misspelled include targets, and avoid clever exists checks whose names are no longer maintained. Keep the policy boring enough that a human can explain every DNS dependency.

Fix the names that should never have been queried, not the receiver’s enforcement. Retire abandoned vendors, correct misspelled include targets, and remove mechanisms left behind by migration. If a legitimate provider currently publishes a negative answer unexpectedly, avoid hard-coding a guessed replacement IP range; escalate to the provider or wait for the DNS issue to be corrected. Recheck after TTL expiry and keep the void-result list in your DNS inventory. This is especially useful for small teams because an old SPF term can sit unnoticed for months until a new provider adds one more lookup and pushes the policy from merely messy into an actual evaluation error.

What this looks like in a small stack

An obsolete include can consume both a normal lookup and a void result in the same evaluation path. A second stale exists term may then trigger the receiver’s void-lookup protection even though the total number of query-producing terms is still under ten.

Void-result tracking belongs in the same change review as ordinary SPF lookup counting. A domain that already has one dead include is telling you its sender inventory has drifted, even if current receivers still return pass for the main stream. Add a simple regression test that resolves every DNS-querying term and reports both the cumulative lookup path and negative answers. Run it after provider migrations and periodically for long-lived domains. If a vendor’s include target becomes empty unexpectedly, preserve the DNS evidence and verify the provider’s current documentation before editing production. The goal is not to chase every transient NXDOMAIN in real time; it is to prevent stale configuration from accumulating until one additional change crosses a standards limit. Separating void lookups from timeouts, SERVFAIL, and positive answers also keeps your verifier from turning a temporary DNS incident into an SPF policy rewrite.

Evidence worth saving for the next incident

Record both counters in the DNS change note: lookup-triggering terms and observed void results. A future provider update can change either count without changing your own top-level TXT string.

When to stop changing things

Do not assume every resolver or receiver enforces the recommendation identically. The portable goal is to remove dead dependencies rather than tune a policy to the most permissive checker you can find.

Field checklist

  • Capture the raw evidence for SPF void lookup limit two PermError before editing DNS, queue state, or mailbox metadata.
  • Confirm the cited mechanism using spf-rfc rather than a generic deliverability score.
  • Keep a known-good control case so the failing layer can be compared without changing several variables at once.
  • Apply the narrow repair described for SPF void lookups: why two empty DNS answers can matter even when you are under 10 lookups and preserve a rollback or retry path.
  • Repeat the original failing test after the change; do not substitute a different checker as proof of recovery.
  • Store timestamp, affected identity, raw result, action taken, and the post-change result in the operator log.

Primary sources

Standards and provider policies can change. These links are the reference points used for this field note.

  1. RFC 7208 — Sender Policy FrameworkIETF / RFC EditorSPF protocol and evaluation semantics.