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 the redirect= modifier. Exceeding the cap returns permerror.
  • ip4, ip6, and all cost 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 include and redirect=. 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 permerror result 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 include chains down to their underlying ip4 / ip6 ranges 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:

SPF mechanism lookup cost (RFC 7208 section 4.6.4)
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:

  1. include:_spf.google.com costs 1 (the top-level include) + the recursive cost of _spf.google.com's record, which is v=spf1 include:_netblocks.google.com include:_netblocks2.google.com include:_netblocks3.google.com ~all - that is 3 more includes, each of which is a leaf ip4:/ip6: list. Subtotal: 1 + 3 = 4 lookups.
  2. include:spf.protection.outlook.com costs 1 (the top-level include) + the recursive cost of spf.protection.outlook.com, which is currently a single ip4: / ip6: list with no further includes. Subtotal: 1 + 0 = 1 lookup.
  3. include:spf.sendgrid.net costs 1 + 1 (the underlying record uses include:sendgrid.net). Subtotal: 2 lookups.
  4. include:mailgun.org costs 1 + 1 (the underlying record uses include:_spf.eu.mailgun.org or similar). Subtotal: 2 lookups.
  5. include:_spf.salesforce.com costs 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.net from 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.com instead of include:_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 ptr mechanisms. Operators who inherited old SPF records with ptr mechanisms targeting a previous hosting provider's IP space whose reverse-DNS no longer resolves.
  • Bare a mechanisms with no A/AAAA record. A bare a:mail.example.com trips a void lookup if mail.example.com exists 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:

Approximate include cost for popular SaaS senders (2026)
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=permerror at every receiver. The header typically reads spf=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=permerror in the per-row auth_results block, with the DMARC policy_evaluated disposition degraded to whatever the published p= 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 uses 5.7.1 Unfortunately, messages from [IP] weren't sent; Yahoo uses 553 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:

  1. Pull 30 days of DMARC aggregate (RUA) data from DMARC Analyzer's dashboard.
  2. 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.
  3. Stage the change in a test domain first. Publish the slimmer record on test-spf.example.com and send test traffic through each remaining sender. Verify every sender still passes.
  4. 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), or Net::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 ptr mechanisms. 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 -all in 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 +all as a terminator. +all authorises 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 the redirect= modifier consumes 1 lookup; ip4, ip6, and all cost zero. Exceeding the cap returns permerror, 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.

Check my SPF lookup count Sign up — Free plan