SPF is recursive, and recursion over DNS at the internet’s scale needs a bound or a single record could make a receiver perform unlimited work. So the specification imposes two limits. Nearly everyone knows about the first. The second is in the same section and catches people who have carefully counted to ten.
The ten-lookup limit
RFC 7208 §4.6.4 permits at most ten mechanisms that require a DNS lookup during a single evaluation. Exceeding it is a permerror — a hard failure. SPF cannot pass for any host, including the hosts explicitly listed.
| Counts toward the limit | Does not count |
|---|---|
include — one each, plus whatever the included record costs | ip4 and ip6 — no lookup at all |
a — one each | all |
mx — one each | The initial lookup of the record itself |
ptr and exists — one each | |
redirect — one |
The count is cumulative across the whole tree, not per record. Ten includes at the top level is ten before any of them has been opened.
The cost is not visible in your own record
An include costs one lookup plus the entire cost of the record it points at, and that record belongs to someone else. A platform that restructures its SPF and adds two includes has just spent two of your ten, without telling you, and your record was not edited. Records go over the limit on their own.
The void-lookup limit
In the same section: more than two DNS queries during evaluation that return NXDOMAIN or an empty answer is also a permerror.
This is a separate budget. A record with six lookups, three of which point at names that no longer exist, is under the ten-lookup limit and still permanently broken. Anyone diagnosing that failure by counting to ten will find six, conclude the record is fine, and look somewhere else.
Void lookups accumulate the way you would expect: a retired vendor’s include target that has been deleted, an a: pointing at a hostname from a previous mail platform, an mx on a domain that no longer has MX records.
Counting a record by hand
Take GitHub’s record, which is a good example because it sits close to the limit:
"v=spf1 ip4:192.30.252.0/22 include:spf.protection.outlook.com include:_netblocks.google.com include:_netblocks2.google.com include:mail.zendesk.com include:_spf.salesforce.com include:servers.mcsv.net include:mktomail.com include:sendgrid.net ip4:62.253.2" "27.114 ip4:166.78.69.169 ip4:166.78.69.170 ip4:166.78.71.131 ~all"
- Eight
includeterms: eight lookups. - Then each included record must be opened and its own lookups added. That is where the remaining budget goes, and where it is easy to lose track by hand.
The manual method is: fetch the record, list every lookup-costing term, then fetch each included record and repeat, keeping a running total. It is tedious and entirely doable, and doing it once by hand is worth more than reading about it — mainly because it makes obvious how much of your budget is spent inside other people’s records.
What to do when you hit it
- Remove senders you no longer use. Always first. Most over-limit records contain at least one vendor nobody has used in two years, and those are also the likeliest source of void lookups.
- Move senders to a subdomain. A platform sending as
mail.example.comneeds an include in that domain’s record, not the parent’s. This is the cleanest structural fix and it also improves your DMARC reporting later. - Replace an include with the addresses behind it — flattening. It works, and it converts a loud failure into a silent one: when the platform changes its addresses, your record is wrong and nothing tells you. Only do this with automation that re-checks.
- Use
existswith a dynamic backend, if your DNS provider supports it. One lookup regardless of how many senders you have.