SPF 10 DNS Lookup Limit and SPF Flattening Explained (RFC 7208 section 4.6.4)
A focused deep dive on the SPF 10-DNS-lookup limit defined in RFC 7208 section 4.6.4: why the limit exists, how lookups are counted per mechanism, the recursive include resolution that catches operators by surprise, the 2-void-lookup secondary cap, an approximate per-provider lookup-cost reference, the canonical symptoms (permerror, DMARC-fail cascade, receiver-specific NDRs), and the four remediation paths in order of preference - audit, subdomain delegation, sender consolidation, and SPF flattening with a monitored pipeline.
This reference is provided in English. The SPF 10-lookup limit is specified in an English-language RFC (RFC 7208 section 4.6.4) and the canonical vocabulary (permerror, void lookup, DNS amplification, flattening, include, redirect=, ip4, ip6) does not translate. We keep the body in the source language so technical terms remain consistent with the specification.
TL;DR
- The SPF 10-lookup limit is defined in
RFC 7208 section 4.6.4:
an SPF evaluation MUST NOT perform more than
10 DNS lookups, counting
include,a,mx,ptr,exists, and theredirect=modifier. Exceeding the cap returnspermerror. ip4,ip6, andallcost zero lookups - they are evaluated against the connecting IP without DNS.- The 10 lookups counter is the SUM across the
ENTIRE evaluation - top-level mechanisms PLUS
every mechanism inside every nested
includeandredirect=. A top-level record with 4 includes whose underlying records each contain 3 more includes consumes 4 + 12 = 16 lookups even though the published record only shows 4. - RFC 7208 section 4.6.4 paragraph 3 adds a secondary cap of 2 void lookups: an SPF evaluation that encounters two or more DNS queries returning NXDOMAIN or NODATA MAY return permerror.
- A
permerrorresult fails SPF for every recipient and cascades into a DMARC fail on every DMARC-honouring receiver - a single misconfigured include can produce a global mail-delivery outage. - The four standard remediations, in order of preference: (1) audit and remove unused includes, (2) split senders across subdomains, (3) consolidate to fewer ESPs, (4) flatten with a monitored pipeline. Macros (RFC 7208 section 7) are a distant fifth.
- SPF flattening is the practice
of resolving
includechains down to their underlyingip4/ip6ranges and republishing the flattened record. It trades one operational cost (a daily / hourly cron to re-resolve includes) for one architectural risk (the flattened record diverges from the upstream sender's truth between refreshes).
1. Overview: what the limit is and why every operator hits it
Sender Policy Framework (SPF)
works by publishing a list of authorised sending
sources in a TXT record on the sending domain. A
receiver fetches the SPF record and walks the
mechanisms one by one to decide whether the
connecting IP is on the authorised list. Some
mechanisms - ip4, ip6,
all - test the connecting IP directly
against a hard-coded value and require no DNS.
Other mechanisms - include,
a, mx, ptr,
exists - require the receiver to issue
a DNS query to resolve them.
RFC 7208 section 4.6.4 caps the total number of
those DNS queries at 10 per SPF
evaluation. Exceeding the cap returns
permerror, which fails SPF
authentication for every recipient on the message.
Because DMARC treats SPF permerror as
SPF fail, a record that overshoots
the cap cascades into a DMARC fail on every
receiver that honours DMARC - which today means
every major mailbox provider and every enterprise
mailbox running a modern security gateway.
The 10-lookup limit is the single most common
cause of SPF failure in production. Operators
routinely hit it by adding one more
include: for one more SaaS sender
(a new survey tool, a new help-desk product, a
new marketing automation platform) without
realising that the new include's UNDERLYING
record already contains several more includes
that count against the parent domain's budget.
The published record shows 5 includes; the
actual lookup cost is 14.
This guide walks the rules behind the limit, the per-mechanism cost, the recursive cost accounting that catches operators by surprise, the void-lookup secondary cap, a per-provider lookup-cost reference, the symptoms and cascading effects of permerror, and the four canonical remediation paths.
2. Why the limit exists: RFC 7208 section 4.6.4 historical context
The cap is not arbitrary. SPF was originally designed in RFC 4408 (2006) with a 10-lookup cap and inherited unchanged into RFC 7208 (2014) section 4.6.4. The cap exists because without it, SPF would be weaponisable into a DNS amplification denial-of-service vector against arbitrary third-party DNS infrastructure.
The attack works as follows. An attacker registers
evil.example and publishes an SPF
record consisting of nested includes that
eventually point at a victim domain's DNS
infrastructure. Each time a mail receiver
evaluates SPF for a message claiming to be from
evil.example, that receiver issues a
cascading flood of DNS queries against the
victim's authoritative nameservers. The attacker
only has to send one message to each of N mail
receivers; the receivers do all the work. Without
a cap on the number of DNS queries per evaluation,
a single attacker with access to a few hundred
open relays could trivially exhaust the victim's
DNS infrastructure.
The 10-lookup cap exists to bound this attack surface. It is a deliberate trade-off: SPF cannot represent arbitrarily complex sender topologies, and operators with many senders MUST use subdomain delegation or flattening to stay under cap. RFC 7208 section 4.6.4 paragraph 1 is explicit:
Some mechanisms and modifiers (collectively, "terms") cause DNS queries
at the time of evaluation, and some do not. The following terms cause
DNS queries: the "include", "a", "mx", "ptr", and "exists" mechanisms,
and the "redirect" modifier. SPF implementations MUST limit the total
number of those terms to 10 during SPF evaluation, to avoid unreasonable
load on the DNS. If this limit is exceeded, the implementation MUST
return "permerror".
"Unreasonable load on the DNS" is the operative phrase. The cap is not about SPF semantic complexity; it is about protecting third-party DNS infrastructure from being weaponised by SPF evaluators on behalf of attackers.
3. Per-mechanism lookup cost
Every SPF mechanism falls into one of three cost classes: zero-cost, one-cost, or recursive. The table below summarises the cost of each:
| Mechanism | Cost | Notes |
|---|---|---|
ip4: |
0 | Tests the connecting IP against a literal IPv4 address or CIDR range. No DNS query. |
ip6: |
0 | Tests the connecting IP against a literal IPv6 address or CIDR range. No DNS query. |
all |
0 | Catch-all terminator. Always matches; the qualifier (+, ~, -) determines the result. |
a |
1 | Resolves the A/AAAA record of the current domain (or an explicit a:host argument). One lookup per query. |
mx |
1 + N | Resolves the MX record list (1 lookup), then resolves the A/AAAA of EACH MX host. RFC 7208 section 4.6.4 paragraph 4 caps the per-mechanism MX target count at 10. |
ptr |
1 + N | Reverse-resolves the connecting IP via PTR, then verifies each PTR target via forward A/AAAA. RFC 7208 section 5.5 deprecates ptr - never use it. |
exists |
1 | Resolves an A record at the named host; matches if any A record exists. Often used with macros. |
include: |
1 + recursive | One lookup for the included record, plus the FULL recursive cost of every mechanism inside the included record. |
redirect= |
1 + recursive | One lookup for the redirected record, plus the FULL recursive cost of every mechanism inside it. Only one redirect per record per RFC 7208 section 6.1. |
The two cost classes that catch operators by
surprise are mx with many MX hosts
and include: with deeply nested
underlying records. An mx mechanism
against a domain with 5 MX hosts costs 6 lookups
(1 for the MX query, 5 for the A/AAAA queries on
each host). An include: against a
SaaS sender whose record contains 4 more includes
costs at least 5 lookups (1 for the top-level
include, 4 for the nested includes, more if those
nested includes themselves contain more).
4. Recursive include resolution: the hidden cost
Consider a record that looks innocent:
example.com. IN TXT "v=spf1 include:_spf.google.com include:spf.protection.outlook.com include:spf.sendgrid.net include:mailgun.org include:_spf.salesforce.com -all"
Five includes. The published record's mechanism
count is 5. The ACTUAL DNS-lookup cost is
significantly higher because each include:
causes the receiver to fetch the underlying record
and recursively count every mechanism inside it.
Walk it through, one include at a time:
include:_spf.google.comcosts 1 (the top-level include) + the recursive cost of_spf.google.com's record, which isv=spf1 include:_netblocks.google.com include:_netblocks2.google.com include:_netblocks3.google.com ~all- that is 3 more includes, each of which is a leafip4:/ip6:list. Subtotal: 1 + 3 = 4 lookups.include:spf.protection.outlook.comcosts 1 (the top-level include) + the recursive cost ofspf.protection.outlook.com, which is currently a singleip4:/ip6:list with no further includes. Subtotal: 1 + 0 = 1 lookup.include:spf.sendgrid.netcosts 1 + 1 (the underlying record usesinclude:sendgrid.net). Subtotal: 2 lookups.include:mailgun.orgcosts 1 + 1 (the underlying record usesinclude:_spf.eu.mailgun.orgor similar). Subtotal: 2 lookups.include:_spf.salesforce.comcosts 1 + ~3 (Salesforce's record contains ~3 more includes for various regions). Subtotal: 4 lookups.
Total: 4 + 1 + 2 + 2 + 4 = 13 lookups.
The published record shows 5 mechanisms; the
receiver pays for 13. The record is in
permerror on every receiver that
walks it.
This is the canonical operator surprise: the visible mechanism count understates the actual cost by 2x to 4x. The only way to know the real cost is to walk every nested include yourself or use a tool that does. DMARC Analyzer's Domain Security Checker reports the live recursive lookup count for any domain.
Worse, the cost can change without the operator
touching anything: when a SaaS sender adds a new
region (Google adds _netblocks4.google.com,
Salesforce adds a new geography), every customer
record that includes them pays the extra lookup.
Records that were comfortably at 8 lookups on a
Tuesday can hit 11 on a Wednesday because of a
supplier-side change the operator never saw. This
is why monitoring the live lookup count (not just
the static record text) matters.
5. The secondary cap: 2 void lookups
RFC 7208 section 4.6.4 paragraph 3 specifies a secondary, less-known cap:
SPF implementations SHOULD limit "void lookups" to two. Should this
limit be exceeded, the implementation MAY return "permerror".
The purpose of this limit is to prevent attackers from creating SPF
records that cause excessive DNS lookups for nonexistent domains.
A void lookup is any DNS query that returns either NXDOMAIN (the domain does not exist) or NODATA (the domain exists but has no records of the requested type). The void-lookup cap is independent of the 10-lookup cap and is typically tripped by SPF records that include or reference domains that have been retired.
Common void-lookup sources:
- Retired SaaS senders. An
operator switches from MailChimp to Klaviyo
but forgets to remove
include:servers.mcsv.netfrom their SPF record. MailChimp keeps the underlying record alive for years but eventually retires it - and every SPF evaluation thereafter trips a void lookup. - Typo-squatted includes. An
operator typed
include:_spf.googl.cominstead ofinclude:_spf.google.com. The record was published, no one noticed because the operator had OTHER passing mechanisms, and the typo trips a void lookup on every evaluation. - Discontinued
ptrmechanisms. Operators who inherited old SPF records withptrmechanisms targeting a previous hosting provider's IP space whose reverse-DNS no longer resolves. - Bare
amechanisms with no A/AAAA record. A barea:mail.example.comtrips a void lookup ifmail.example.comexists but only has AAAA, or only A, or neither.
Two void lookups MAY cause permerror under RFC 7208. Major receivers (Google, Microsoft, Yahoo) are increasingly strict about enforcing the cap - what was a SHOULD in 2014 is now a de-facto MUST in 2026. The remediation is always to audit the record and remove the broken references.
6. Per-provider lookup cost reference (approximate, 2026)
Lookup costs are not stable over time - SaaS providers add and remove netblock subqueries as their infrastructure evolves. The figures below are approximate as of mid-2026 and should be re-verified against the live record before relying on them for capacity planning:
| Provider | Canonical include | Approx. lookups |
|---|---|---|
| Google Workspace | include:_spf.google.com |
~4 (1 top + 3 netblocks) |
| Microsoft 365 / Exchange Online | include:spf.protection.outlook.com |
~1 (single ip4/ip6 list) |
| Amazon SES | include:amazonses.com |
~1-2 (regional) |
| SendGrid | include:sendgrid.net |
~2 (1 top + 1 nested) |
| Mailgun | include:mailgun.org |
~2-3 (regional) |
| Postmark | include:spf.mtasv.net |
~1 |
| Salesforce | include:_spf.salesforce.com |
~4 (regional shards) |
| HubSpot | include:spf.hubspotemail.net |
~2 |
| Mailchimp | include:servers.mcsv.net |
~1 |
| Klaviyo | include:_spf.klaviyo.com |
~2 |
| Constant Contact | include:spf.constantcontact.com |
~2 |
| Zoho Mail | include:zoho.com |
~2 |
| Brevo (Sendinblue) | include:spf.brevo.com |
~2 |
A naive operator who combines Google Workspace + Microsoft 365 + SendGrid + Mailgun + Salesforce + HubSpot pays approximately 4 + 1 + 2 + 2 + 4 + 2 = 15 lookups - already over cap before any of their own transactional senders, marketing senders, or on-prem MTA. This combination is extremely common in mid-market SaaS companies, and the resulting permerror is one of the most common root causes cited in DMARC monitoring reports.
7. Symptoms of exceeding the limit
The canonical symptoms of an over-budget SPF record are:
- Authentication-Results header shows
spf=permerrorat every receiver. The header typically readsspf=permerror (sender SPF authorization permerror) smtp.helo=...; ... reason="too many DNS lookups"on Google receivers and analogous text on Microsoft, Yahoo, etc. - DMARC aggregate (RUA) reports show
spf=permerrorin the per-rowauth_resultsblock, with the DMARCpolicy_evaluateddispositiondegraded to whatever the publishedp=tag demands. - Receiver-specific NDRs.
Bounces vary widely: Google often issues
550 5.7.26 This mail has been blocked because the sender is unauthenticated; Microsoft uses5.7.1 Unfortunately, messages from [IP] weren't sent; Yahoo uses553 5.7.1 [BL21] Messages from this sender did not pass DMARC. - Sudden, mass delivery failure for ONE specific sending source while other sources continue working. The permerror cascade affects EVERY recipient of every message from the affected From: domain, but it only surfaces when SPF was the load-bearing identifier - mail that was also DKIM-aligned will still pass DMARC.
- Sender reputation degradation. Receivers track per-domain authentication pass rates over time. A domain with sustained permerror gets demoted in deliverability ranking even at receivers that do not strictly enforce DMARC.
The diagnostic is straightforward: pull the
Authentication-Results header from a failing
message and look for the literal string
spf=permerror. If present,
investigate the SPF record's live lookup count;
if it is over 10 or has more than one void
lookup, that is the root cause.
8. The four remediation paths (in order of preference)
8.1 Audit and remove unused includes
The cheapest, lowest-risk remediation is to delete includes for senders the organisation no longer uses. Operators routinely retain includes for SaaS senders that were sunset years ago because removing them feels risky. The procedure is:
- Pull 30 days of DMARC aggregate (RUA) data from DMARC Analyzer's dashboard.
- For each included sender (Salesforce, SendGrid, etc.), check whether ANY messages in the 30-day window claim that sender's IP space. If none, the include is dead and can be removed.
- Stage the change in a test domain first.
Publish the slimmer record on
test-spf.example.comand send test traffic through each remaining sender. Verify every sender still passes. - Publish to production. Monitor the next 24-48h of RUA reports for any unexpected SPF fails.
Typical impact: removing 2-4 dead includes reclaims 4-12 lookups depending on the recursive cost of the removed includes.
8.2 Subdomain delegation
Split sending sources across different subdomains. Each subdomain has its own 10-lookup budget. This is the canonical scaling pattern for organisations with many sending sources:
example.com. IN TXT "v=spf1 include:_spf.google.com -all"
news.example.com. IN TXT "v=spf1 include:sendgrid.net -all"
mail.example.com. IN TXT "v=spf1 include:mailgun.org -all"
crm.example.com. IN TXT "v=spf1 include:_spf.salesforce.com -all"
Each sender's mail uses the From: header
matching its subdomain (marketing@news.example.com,
transactional@mail.example.com, crm@crm.example.com),
and each subdomain's SPF record stays well under
the 10-lookup cap. The parent
example.com only carries staff mail
via Google Workspace.
Subdomain delegation also improves DMARC reporting clarity: the rua= reports identify the sub-domain that produced each message, so an auth failure spike is immediately attributable to a specific sender.
Costs of this approach: every sender has to be
reconfigured to send from its subdomain (not
always possible if the marketing team insists on
sending from the bare example.com);
DKIM records must be published for each
subdomain; DMARC records can either be inherited
(the parent's p= applies via the
organisational-domain rule) or published
per-subdomain for finer control.
8.3 Sender consolidation
If an organisation has duplicate-purpose senders (two transactional ESPs, three marketing platforms), consolidating to fewer senders reclaims lookups AND reduces vendor sprawl. A company that uses SendGrid, Mailgun, AND Postmark for transactional mail is almost certainly not getting differentiated value from each; picking one and migrating the others typically reclaims 2-4 lookups and simplifies operations.
8.4 Flattening
The fourth remediation path - and the one most commonly reached for by tools - is SPF flattening. See the dedicated section below.
8.5 (Distant fifth) Macros
SPF macros (RFC 7208 section 7) let an SPF
record substitute runtime variables (the
connecting IP, the HELO name, the sender domain)
into mechanism arguments, which CAN reduce
lookups by replacing N includes with one
exists: check against a macro-built
lookup name (e.g.
exists:%{i}.spf.example.com resolves
the connecting IP into a per-IP lookup that
consumes 1 lookup regardless of how many
authorised IPs the operator publishes). In
practice macros are rare in production because
they are hard to reason about, hard to debug,
and have inconsistent support across legacy SPF
evaluators. Reach for them only after the first
four remediation paths have been exhausted.
9. SPF flattening in depth
9.1 What flattening is
SPF flattening is the practice of periodically
resolving every include,
a, and mx mechanism in
an SPF record down to its underlying ip4 and ip6
ranges, then republishing the record as a
list of ip4: and ip6:
mechanisms that cost zero lookups. A 13-lookup
record becomes a 0-lookup record at the cost of
a much longer DNS payload.
Before flattening:
example.com. IN TXT "v=spf1 include:_spf.google.com include:spf.protection.outlook.com -all"
After flattening:
example.com. IN TXT "v=spf1 ip4:35.190.247.0/24 ip4:64.233.160.0/19 ip4:66.102.0.0/20 ip4:66.249.80.0/20 ip4:72.14.192.0/18 ip4:74.125.0.0/16 ip4:108.177.8.0/21 ip4:173.194.0.0/16 ip4:209.85.128.0/17 ip4:216.58.192.0/19 ip4:216.239.32.0/19 ip4:40.92.0.0/15 ip4:40.107.0.0/16 ip4:52.100.0.0/14 ip4:104.47.0.0/17 -all"
The flattened record costs 0 DNS lookups (all
mechanisms are ip4: /
ip6: literals) but the operator now
owns the responsibility of keeping the
ip4: list in sync with Google's and
Microsoft's actual netblocks.
9.2 When to use flattening
Flattening is appropriate when:
- The organisation has exhausted the first three remediation paths (audit, subdomain delegation, consolidation) and is STILL over cap.
- The senders being flattened are stable - Microsoft 365's netblocks change rarely; SendGrid's regional shards change more often. Flattening volatile senders means more frequent refresh cycles.
- The operator has the operational capacity to run, monitor, and alert on the refresh pipeline. Flattening without monitoring is a silent time bomb.
9.3 When NOT to use flattening
Flattening is INAPPROPRIATE when:
- The organisation has not yet audited unused includes. Flattening 14 includes when 4 are dead is paying operational cost for value the audit could deliver for free.
- The senders being flattened are highly volatile (a sender that rotates IPs hourly cannot be safely flattened on a daily cron).
- The operator does not have the operational capacity to monitor the refresh pipeline. A flattened record that goes stale silently fails SPF for whichever sender's IPs rotated out of the cached list.
9.4 Manual flattening procedure
The minimal viable manual flattening procedure using standard Unix tooling:
# Resolve every nested include down to ip4/ip6 mechanisms.
# Requires the python3-dnspython package.
python3 -c "
import dns.resolver
def walk(domain, depth=0):
if depth > 5: return []
try:
ans = dns.resolver.resolve(domain, 'TXT')
except Exception: return []
out = []
for r in ans:
txt = b''.join(r.strings).decode()
if not txt.startswith('v=spf1'): continue
for term in txt.split()[1:]:
if term.startswith('ip4:') or term.startswith('ip6:'):
out.append(term)
elif term.startswith('include:'):
out.extend(walk(term.split(':',1)[1], depth+1))
return out
print(' '.join(walk('_spf.google.com')))
"
Wrap this in a cron job that runs every 24 hours, computes a hash of the new flattened record, compares it to the published record, and either republishes via your DNS provider's API or alerts on divergence.
9.5 Commercial flattening services
Several vendors offer SPF flattening as a service:
- Valimail Authenticate - managed SPF + DKIM + DMARC service with automatic flattening.
- PowerDMARC - SPF flattening service with hosted records and automatic refresh.
- EasyDMARC EasySPF - hosted SPF flattening with a managed record endpoint.
- dmarcian SPF Magic - flattening service tied to the dmarcian DMARC platform.
- Red Sift OnDMARC - includes an SPF flattening / Dynamic SPF feature.
These services typically work by having the
operator publish an include: to a
vendor-hosted record (e.g.
include:_spf.vendor.com) that the
vendor keeps continuously updated. The operator
delegates the refresh problem to the vendor at
the cost of one inbound dependency and one
vendor lock-in vector.
9.6 Self-hosted flattening
Open-source flattening implementations exist for operators who prefer self-hosting:
- SPFlat - reference Python implementation, well-suited to a cron-driven pipeline.
- Custom scripts using
dnspython(Python),miekg/dns(Go), orNet::DNS(Perl). The walk algorithm is straightforward; the operational complexity is in the publish-and-monitor pipeline, not the resolver code.
9.7 Flattening pitfalls
The five canonical pitfalls of flattening:
- Stale flattened record. The cron job failed three weeks ago, the operator never noticed, and a SaaS sender rotated their IP range last Tuesday. Mail from that sender silently fails SPF until someone debugs it.
- TXT record length limits.
A single TXT record is capped at 255 bytes
per string; the total record (concatenated
strings) is capped by the DNS UDP/TCP
message size. Flattened records of 15+
mechanisms approach the limit. Splitting
into multiple TXT records with
include:chains DEFEATS the flattening because the includes count against the lookup budget. - Refresh interval too long. A daily refresh interval is too slow for senders that rotate IPs hourly. The canonical examples are CDN-fronted ESPs that elastically expand and contract their sending infrastructure.
- Divergent upstream truth. The flattened record represents what the operator's pipeline saw at the last refresh. It is NOT the upstream sender's authoritative list. If the upstream sender adds an IP to an ip4 mechanism IN its source record and the receiver evaluates the live record (because the operator is NOT flattening but a friend is), the operator's flattened record will lag.
- Vendor lock-in via hosted flattening. Switching from Vendor A's hosted flattening to Vendor B's requires changing the production SPF record - a high-blast-radius operation. Pick carefully and confirm the vendor has a documented data-export path.
10. Best practices
- Audit before flattening. Remove dead includes first. Flattening pays operational cost; an audit is free.
- Monitor the live lookup count. The static record text underreports the cost by 2x to 4x. Set up a daily check that recursively counts lookups and alerts on >= 9 (one below cap, so you have headroom before permerror hits production).
- Never publish bare
ptrmechanisms. RFC 7208 section 5.5 deprecates them; receivers treat them with suspicion; and they trip void lookups whenever reverse-DNS is stale. - Always terminate with
-allin production.~all(softfail) is acceptable only during the first 2-4 weeks of an initial rollout while shadow senders are being discovered. - Stage SPF changes in a test
domain first. Publish on
test-spf.example.com, route test traffic through each sender, verify every authorised source still passes, only then promote to production. - Document the senders. Maintain a list of every include and what it is for. When someone leaves and a replacement is hired, the list answers the "can I delete this?" question without archaeology.
- Prefer subdomain delegation over flattening when adding new senders. Delegation scales cleanly with sender count; flattening accumulates operational debt.
- If flattening, monitor the pipeline. A failed cron job that does not alert on-call is a silent ticking bomb. Alert on cron failure AND on stale record age (e.g. "flattened record has not been refreshed in 48 hours").
11. Common mistakes
- Counting mechanisms instead of lookups. The published record's visible mechanism count is not the same as the lookup cost. A record with 5 includes can easily consume 15 lookups.
- Adding one more SaaS include without checking the cost. Every new SaaS sender's include should be cost-checked against the current budget BEFORE it goes into production.
- Splitting a long flattened record
into chained includes. Splitting
a single 800-byte flattened TXT into two
TXT records via
include:defeats the flattening - the include itself consumes 1 lookup and so does the chained record. - Using
+allas a terminator.+allauthorises every IP on the internet to send mail as your domain - it is functionally equivalent to having no SPF record at all and is exploited by spammers within hours of publication. - Leaving retired senders' includes in place. Every retired sender's include continues to cost lookups (and risks void lookups when the upstream record finally goes away). Audit and remove them aggressively.
- Treating SPF as the only authentication mechanism. SPF breaks during forwarding. DKIM does not. Every sender should be DKIM-signed with custom-domain DKIM enabled so DMARC alignment survives the forwarding hop.
- Trusting MX flattening tools to be correct forever. A vendor-hosted flattening service still requires the operator to monitor the vendor's status - vendor outages cascade into the operator's mail flow.
12. Troubleshooting
12.1 "spf=permerror" appearing in Authentication-Results
The Authentication-Results header reads
spf=permerror. Confirm the cause by
walking the SPF record recursively and counting
lookups. If the count is over 10, that is the
cause. If the count is at or below 10, check the
void-lookup count - any single mechanism whose
underlying domain returns NXDOMAIN or NODATA
counts. Two or more void lookups MAY trip
permerror.
DMARC Analyzer's Domain Security Checker reports both counts. The Email Header Analyzer parses Authentication-Results headers and surfaces the permerror reason text from the receiver.
12.2 SPF passes but DMARC fails
SPF passed for the envelope domain but the envelope domain does not align with the From: header domain. This is the DMARC alignment problem, not the SPF lookup-limit problem. See DMARC alignment explained for the alignment rules. The fix is usually enabling custom-domain DKIM at the SaaS sender so DKIM alignment carries the message.
12.3 Permerror at some receivers, no error at others
Different SPF evaluators differ in how strictly they enforce the 10-lookup cap. Older implementations may silently continue past 10; newer implementations reject. The published record is the same; the operator only saw permerror reports from the strict receivers. The remediation is the same regardless of which receiver reported it - the record IS over budget and needs to be fixed.
12.4 The lookup count changed without my touching the record
A SaaS sender added or removed an internal include subquery in their canonical record. Your include's recursive cost changed without any change to your published record. This is one of the strongest arguments for monitoring the live lookup count rather than the static record text.
13. Frequently asked questions
- What is the SPF 10 DNS lookup limit?
- RFC 7208 section 4.6.4 caps the number of
DNS lookups an SPF evaluation may perform at
10. Each
include,a,mx,ptr,exists, and theredirect=modifier consumes 1 lookup;ip4,ip6, andallcost zero. Exceeding the cap returnspermerror, which fails SPF for every recipient and cascades into a DMARC fail on every DMARC-honouring receiver. - How do I check how many SPF lookups my record uses?
- Use a recursive SPF resolver that walks every include, redirect=, a, mx, ptr, and exists mechanism and counts each DNS query against the 10-lookup budget. DMARC Analyzer's Domain Security Checker reports the live lookup count and per-include attribution. Manual counting is unreliable because nested includes inflate the total in ways that do not appear in the raw record text.
- What is SPF flattening?
- SPF flattening is the practice of periodically resolving every include, a, and mx mechanism in an SPF record down to its underlying ip4 and ip6 ranges, then republishing the flattened record as a list of ip4/ip6 mechanisms that cost zero lookups. Flattening trades operational cost (a recurring cron job) for architectural risk (the flattened record diverges from the upstream sender's real IP set between refreshes).
- What is the void lookup limit?
- RFC 7208 section 4.6.4 paragraph 3 specifies a secondary cap of 2 void lookups: an SPF evaluation that encounters two or more DNS queries returning NXDOMAIN or NODATA MAY return permerror. The cap is independent of the 10-lookup cap.
- Is SPF flattening safe?
- Flattening is safe IF the operator commits to two operational practices: (1) re-resolve and republish the flattened record at least daily, and (2) monitor the flattening pipeline so a failed cron job alerts on-call before the flattened record goes stale. Flattening published once and forgotten is a silent time bomb.
- What happens when SPF returns permerror?
- A permerror result fails SPF authentication for every recipient processed by every receiver that respects RFC 7208. The DMARC evaluation then sees SPF=fail and combines it with the DKIM result: if DKIM also fails or is not aligned, DMARC fails and the receiver applies the published policy.
- How does subdomain delegation reduce SPF lookup count?
- Subdomain delegation splits sending sources across different subdomains, each of which publishes its own SPF record with only the includes that subdomain actually uses. Each subdomain has its own 10-lookup budget, so 30 underlying lookups split 3 ways stay comfortably under cap.
- Should I use SPF macros to reduce lookups?
- SPF macros (RFC 7208 section 7) CAN reduce lookups but are rare in production because they are hard to reason about and debug. Reach for them only after the four other remediation paths have been exhausted.
15. Audit your SPF lookup count now
DMARC Analyzer's
Domain Security Checker
performs a live recursive walk of any published
SPF record (via
includes/DnsChecker.php), reports
the total lookup count, attributes each lookup
to its source mechanism, and flags any record
that is at or over the 10-lookup cap. The
Email Header Analyzer
parses Authentication-Results headers from any
message (via
includes/EmailHeaderParser.php)
and surfaces the
spf=permerror reason text emitted
by the receiving mail server.