When an SPF record exceeds 10 DNS-querying mechanisms, receiving mail servers do not "stop at the 10th include and check the rest." Under RFC 7208 Section 4.6.4, the receiver must immediately abort evaluation and return spf=permerror (Permanent Error).
When spf=permerror occurs, SPF fails completely for every sender in your record—even the very first include: listed at the start of the TXT string. If a message also lacks a valid aligned DKIM signature and your domain enforces DMARC (p=quarantine or p=reject), legitimate business email is quarantined or rejected outright.
You can check your domain's exact lookup count right now with the free SPF Checker or run a full authentication audit with the MailPosture Domain Scanner.
Why the RFC 7208 10-lookup limit exists
SPF evaluation happens during the SMTP transaction while a receiving mail server holds an open connection. Without a strict ceiling on DNS resolution, an attacker could publish a malicious SPF record containing dozens of nested include: and mx mechanisms pointing at a target's authoritative nameservers, turning every receiving mail server into an unwitting DNS amplification reflector.
To prevent denial-of-service attacks and bound SMTP latency, RFC 7208 enforces two separate limits during SPF evaluation:
- The 10-mechanism DNS lookup limit: At most 10 mechanisms or modifiers in the entire evaluation tree (top-level record plus all nested
include:andredirect=targets) may trigger DNS queries. - The 2-void-lookup limit: Receivers are encouraged to abort with
permerrorif more than 2 lookups return an empty answer (NXDOMAINor zero records), which frequently happens when an old SaaS vendor's SPF subdomain is decommissioned.
Which SPF mechanisms count toward the 10-lookup limit?
One of the most common mistakes during DNS cleanup is removing ip4: entries thinking they cost DNS lookups, while leaving nested include: and mx tags intact.
| SPF Mechanism / Modifier | Counts Toward 10-Lookup Limit? | Why / How Many Lookups It Costs |
|---|---|---|
v=spf1 | No (0 lookups) | Version prefix; evaluated locally from the initial TXT fetch. |
ip4: / ip6: | No (0 lookups) | Literal CIDR blocks (e.g., ip4:198.51.100.0/24) require zero DNS queries. |
all (-all, ~all, ?all) | No (0 lookups) | Catch-all terminal mechanism; evaluated without DNS resolution. |
include: | Yes (1 + nested lookups) | Queries the target domain's TXT record plus every lookup inside that target's SPF record. |
a | Yes (1 lookup) | Queries A/AAAA records for the current domain or specified host. |
mx | Yes (1 + MX host lookups) | Queries the MX record (1 lookup), then resolves A/AAAA for each MX hostname. |
ptr | Yes (Deprecated) | Triggers reverse DNS queries; explicitly discouraged by RFC 7208. |
exists: | Yes (1 lookup) | Queries an A record on a macro-expanded domain. |
redirect= | Yes (1 + target lookups) | Replaces current evaluation with the target domain's SPF policy. |
How a normal SaaS stack silently hits 12+ lookups
Modern companies rarely send email from a single mail server. Consider a typical B2B domain using Google Workspace for corporate mail, HubSpot for marketing, Zendesk for support tickets, SendGrid for product notifications, and an mx mechanism copied from an old tutorial:
v=spf1 a mx include:_spf.google.com include:mail.zendesk.com include:sendgrid.net include:123456.spf01.hubspotemail.net ~all
At first glance, there are only 4 include: statements. Why does this record fail with permerror? Because nested lookups are recursive:
a= 1 lookupmx= 1 lookup (plus A-record resolution for the MX hostnames)include:_spf.google.com= 4 lookups (_spf.google.comchains into_netblocks.google.com,_netblocks2.google.com, and_netblocks3.google.com)include:mail.zendesk.com= 1 lookupinclude:sendgrid.net= 1 to 2 lookups (sendgrid.netchains intoab.sendgrid.net)include:123456.spf01.hubspotemail.net= 1 to 2 lookups
Total: 9 to 11+ DNS lookups. The moment one of those cloud providers adds a single new regional netblock include: to their own SPF record, your domain crosses 10 lookups overnight without anyone on your team touching DNS.
Step-by-step: How to reduce SPF lookups below 10 safely
Rather than reaching immediately for brittle IP flattening scripts, work through these five engineering steps in order.
1. Remove redundant a, mx, and ptr mechanisms
In 90% of cloud-hosted domains, a (your web server IP, such as Vercel, Cloudflare, or AWS CloudFront) never sends outbound SMTP mail. Similarly, if you already include _spf.google.com or spf.protection.outlook.com, adding mx is completely redundant because your MX servers are already covered by the provider's include:.
Removing a and mx immediately saves 2+ lookups with zero impact on legitimate mail.
2. Audit and prune inactive third-party include: entries
Every marketing platform, billing tool, or helpdesk trialed over the last five years tends to leave an include: behind in your root TXT record. Cross-reference your current SPF record against your DMARC aggregate (RUA) reports over a 14-day window:
- If a vendor's
include:has sent zero messages in 30 days, confirm with stakeholders and remove it. - Pay special attention to decommissioned vendors whose SPF subdomains return
NXDOMAIN, as two dead includes can trigger the RFC 7208 void-lookup failure even if your total count is under 10.
3. Move bulk, marketing, and transactional senders to dedicated subdomains
This is the cleanest architectural fix used by mature security teams. SPF is evaluated against the RFC5321.MailFrom (Return-Path / bounce) domain, not necessarily your organizational root domain.
Instead of cramming every ESP into @ (yourdomain.com), segment traffic by function:
- Root domain (
yourdomain.com): Reserved for employee mail (Google Workspace or Microsoft 365). - Marketing subdomain (
mail.yourdomain.comornews.yourdomain.com): Publishes its own SPF record with HubSpot, Marketo, or Mailchimp. - Transactional subdomain (
notify.yourdomain.com): Publishes its own SPF record for SendGrid, Postmark, Amazon SES, or Resend.
Even if your visible From: header remains support@yourdomain.com, configuring a custom Return-Path subdomain (e.g., bounces.yourdomain.com) in your ESP shifts the SPF lookup to bounces.yourdomain.com while still passing relaxed DMARC SPF alignment (aspf=r). See our SPF, DKIM, and DMARC alignment troubleshooting guide for exact header examples.
4. Prefer DKIM-aligned providers that manage their own Return-Path
Many modern email service providers (including Postmark, Amazon SES, Mailchimp, and Klaviyo) authenticate via DKIM against your domain (d=yourdomain.com) while using their own default or custom bounce subdomain for SPF. When a provider sets its own Return-Path subdomain, you do not need to add their include: to your root domain's SPF record at all—their SPF record lives on the bounce subdomain they control.
5. Replace static single-IP includes with literal ip4: / ip6: blocks
If your SPF record includes a custom hostname you control (for example, include:spf-relay.yourdomain.com or a:mail.yourdomain.com) that resolves to one or two static IP addresses on your own infrastructure, replace that mechanism with literal ip4:203.0.113.25 entries. Literal ip4: and ip6: mechanisms cost zero DNS lookups.
Why manual SPF flattening is dangerous
A common workaround is "manual SPF flattening"—resolving a vendor's include:_spf.google.com or include:sendgrid.net into raw ip4: addresses and pasting them directly into your TXT record.
Do not manually flatten third-party cloud providers. Cloud ESPs rotate outbound IP pools and add new CIDR ranges without notice. When Google, Microsoft, or SendGrid shifts traffic to a new IP block not in your flattened list:
- Your static SPF record silently fails (
spf=failorspf=softfail) for messages sent from the new IP range. - You quickly hit the 255-character DNS TXT string limit or the 512-byte UDP DNS packet size threshold.
If you must flatten an over-limit root domain that cannot be segmented into subdomains, monitor the domain continuously so you are alerted the moment lookup counts or upstream records shift.
Before and after: Fixing an 11-lookup SPF record
Before (11 lookups — Fails with PermError):
v=spf1 a mx include:_spf.google.com include:spf.protection.outlook.com include:sendgrid.net include:mail.zendesk.com include:servers.mcsv.net ~all
Why it fails: a (1) + mx (1) + Google (4) + Microsoft (2) + SendGrid (1) + Zendesk (1) + Mailchimp (servers.mcsv.net, which isn't even needed at root if Mailchimp uses its own Return-Path) (1) = 11 lookups.
After (6 lookups — Passes with -all):
v=spf1 include:_spf.google.com include:mail.zendesk.com -all
What changed:
- Removed unused
aand redundantmx(-2 lookups). - Removed legacy
spf.protection.outlook.comleft over from a prior migration (-2 lookups). - Moved
sendgrid.netto a dedicatedem1.yourdomain.comcustom Return-Path CNAME (-1 lookup). - Removed
servers.mcsv.netafter verifying Mailchimp signs with aligned DKIM (k2._domainkey.yourdomain.com) and uses its own bounce domain (-1 lookup). - Upgraded
~allto-allafter verifying clean DMARC aggregate reports.
Verify your SPF lookup count and monitor for drift
Once you update your TXT record, run it through the Free SPF Checker to confirm your lookup count sits safely at 8 or fewer (leaving headroom in case an upstream provider adds a nested netblock). Then run a complete SPF, DKIM, and DMARC scan or set up 24/7 DNS monitoring so you get an instant alert if a vendor change ever pushes your record back over the 10-lookup threshold.