SPF has a silent failure mode that catches out most growing companies exactly when they're expanding their sending stack. The record looks valid. Online validators say it parses. Mail goes out. And yet Gmail logs spf=permerror in the Authentication-Results header on every message, which it treats identically to having no SPF at all. The cause is RFC 7208's hard limit of ten DNS lookups per SPF evaluation, and the stack of modern ESPs that each chew through multiple lookups of their own.
1) Audit your current lookup count with dmarcian or spf-record.com. 2) Remove unused includes (most records have at least one). 3) Either flatten remaining includes to IP lists, or — better — move each vendor onto its own sending subdomain so lookups don't stack. 4) Monitor — flattening drifts as vendor IPs change.
Why the limit exists
When a receiver evaluates SPF, it walks the whole include chain, doing a DNS lookup for each include:, a, mx, exists: and redirect= directive. Without a cap, a malicious actor could construct an SPF tree that forces receivers to make hundreds of lookups per message — a DNS amplification attack. The 10-lookup cap was baked into RFC 4408 (now 7208) as a DoS mitigation. It hasn't moved since. Receivers enforce it strictly; crossing it makes the entire record evaluate to permerror.
What counts toward the limit
These directives consume a lookup each:
include:— one lookup, plus every lookup the included record makes, recursively.aanda:domain— one lookup each.mxandmx:domain— one lookup for the MX query itself, plus one per MX host returned (some receivers).exists:— one lookup.redirect=— one lookup, plus its tree.
These don't count:
ip4:andip6:— literal IPs, no lookup needed.all,~all,-all,?all.
ptr was in the original RFC but is now deprecated — don't use it.
Typical offenders — lookups per include
Common senders, and the lookups they burn when you include them (as of early 2026 — these drift):
- Google Workspace (
_spf.google.com) — 1 lookup. - Microsoft 365 (
spf.protection.outlook.com) — 1 lookup. - SendGrid (
sendgrid.net) — 3 lookups. - Mailgun (
mailgun.org) — 3 lookups. - Amazon SES (
amazonses.com) — 1 lookup. - Mailchimp (
servers.mcsv.net) — 1 lookup. - HubSpot — 1 lookup per region include.
- Marketo (
mktomail.com) — 2 lookups. - Salesforce (
_spf.salesforce.com) — 4 lookups. - Intercom — 1 lookup.
Add any three or four of these together and you're at the cliff. Salesforce + SendGrid + M365 = 8 lookups with nothing else. Add Marketo and HubSpot and you're over.
How to audit your current lookup count
Three options, in order of convenience:
- dmarcian SPF Surveyor. Paste your domain, it returns a tree diagram and a total lookup count. Free for a handful of queries per day.
- spf-record.com. Similar — tree view plus per-include breakdown.
- Manual
dig. Query your SPF, then recursively query every include. Tedious but the only way to be sure which specific include is bloating things on a given day.
dig TXT yourdomain.com +short
dig TXT sendgrid.net +short # follow includes manuallyFix 1 — SPF flattening
Flattening replaces include: references with literal ip4: / ip6: lists. Because IP literals don't count toward the 10-lookup limit, the resulting record evaluates with zero includes.
Pros: immediate fix, works everywhere, simple mental model.
Cons: vendor IPs change. When SendGrid adds a new sending pool or Microsoft rotates their 40.x.x.x ranges, your flattened record silently goes out of date and legitimate mail starts failing SPF. You're on the hook for re-flattening monthly, or paying a service (EasyDMARC, Valimail, PowerDMARC) to monitor and update the record for you.
Fix 2 — Dedicated subdomain per vendor
The cleanest architectural fix. Instead of one omnibus SPF record on yourdomain.com that includes everything, put each sender on its own subdomain:
send.yourdomain.com— Marketo only. SPFv=spf1 include:mktomail.com -all. 2 lookups.mail.yourdomain.com— SendGrid only. SPFv=spf1 include:sendgrid.net -all. 3 lookups.yourdomain.com— M365 (corporate). SPFv=spf1 include:spf.protection.outlook.com -all. 1 lookup.
Pros: each subdomain has plenty of headroom under 10, per-vendor DMARC reports are clean and attributable, no flattening maintenance, vendor IPs can change without breaking you.
Cons: more DNS records to manage, and you have to set the From address correctly in each vendor so SPF aligns against the right subdomain.
Fix 3 — SPF macros
SPF supports macros (%{i}, %{d}) that let you construct dynamic lookup names. In theory you can write a single exists: expression that resolves differently per-sender-IP. In practice this is fragile, opaque, and almost never maintained correctly. Skip unless you genuinely need it.
Fix 4 — Remove unused includes
Almost every SPF record we audit contains at least one include from a service the company stopped using months ago. Survey your actual sending stack this quarter. If you removed Mailgun in favour of Postmark but the SPF still includes mailgun.org, that's three free lookups you can reclaim. This is often the lowest-effort highest-impact fix.
How to verify the fix
After any change, re-run the SPF validator and confirm:
- Total lookup count is ≤ 10 (target ≤ 8 for headroom).
- Record still ends with
-allor~all. - Send a test message and inspect the receiving side's Authentication-Results header:
Authentication-Results: mx.google.com;
spf=pass (google.com: domain of user@yourdomain.com
designates 40.92.0.10 as permitted sender)spf=pass is the target. spf=permerror means you're still over 10; spf=softfail means the sending IP isn't in the record at all.
Flattening solves the lookup problem but introduces a new maintenance burden. Every time your ESP rotates IP ranges, your flattened record drifts. The subdomain strategy avoids this entirely by letting each vendor resolve its own includes at send time. If you're architecting for the long term, start with subdomains.