SPF permerror: too many DNS lookups
What your aggregate report shows
When an SPF evaluation exceeds the DNS lookup limit, the receiving mail server records a permanent error. In your DMARC aggregate report XML, you will see this:
<result>permerror</result>
A permerror means the receiver gave up evaluating your SPF record entirely. It did not pass, it did not fail. It broke. The receiver treats this the same as having no SPF record at all, which means your DMARC SPF check fails. If DKIM also fails or is missing, your message fails DMARC authentication.
The RFC 7208 limit
RFC 7208 section 4.6.4 imposes a hard limit of 10 DNS mechanism lookups during SPF evaluation. The mechanisms that count toward this limit are:
includeamxredirectexists
The mechanisms that do not count are:
ip4ip6all
This is not 10 mechanisms in your record. It is 10 total DNS queries, including recursive lookups triggered by nested include statements. That distinction matters because a single include can consume several lookups on its own.
Diagnosis: counting your SPF lookups
Check your current SPF record
Start by retrieving your domain's SPF record:
dig TXT example.com +short
Look for the record that starts with v=spf1. A typical record might look like this:
v=spf1 include:_spf.google.com include:spf.protection.outlook.com include:sendgrid.net include:spf.mandrillapp.com include:mail.zendesk.com ~all
That is 5 include mechanisms at the top level. Each one costs at least 1 lookup, plus any nested lookups inside it.
How include triggers recursive lookups
Each include mechanism resolves to another SPF record, which may contain its own include, a, or mx mechanisms. These all count toward your total of 10.
Take Google Workspace as an example. When you include _spf.google.com, the receiver resolves it:
dig TXT _spf.google.com +short
"v=spf1 include:_netblocks.google.com include:_netblocks2.google.com include:_netblocks3.google.com ~all"
That is 3 more includes. Each of those resolves to a record containing only ip4 and ip6 mechanisms (which do not count), so the recursion stops there. Total cost for Google Workspace: 4 lookups (1 for _spf.google.com itself, plus 3 for its nested includes).
Common providers and their lookup costs
Here are the typical lookup costs for popular email services. These can change when providers update their SPF records, so verify them periodically.
-
Google Workspace: 4 lookups (
include:_spf.google.com) -
Microsoft 365: 2 lookups (
include:spf.protection.outlook.com) -
Salesforce: 2 lookups (
include:_spf.salesforce.com) -
SendGrid: 1 lookup (
include:sendgrid.net) -
Mailchimp / Mandrill: 2 lookups (
include:spf.mandrillapp.com) -
HubSpot: 1 lookup (
include:spf.hubspot.com) -
Zendesk: 1-2 lookups (
include:mail.zendesk.com) -
Amazon SES: 1 lookup (
include:amazonses.com)
A real-world example that breaks
Here is a record that looks reasonable at first glance:
v=spf1 include:_spf.google.com include:spf.protection.outlook.com include:sendgrid.net include:spf.mandrillapp.com include:mail.zendesk.com include:_spf.salesforce.com ~all
Count the lookups: Google (4) + Microsoft (2) + SendGrid (1) + Mandrill (2) + Zendesk (1) + Salesforce (2) = 12 lookups. That exceeds the limit by 2, and every SPF evaluation will result in permerror.
To count manually, resolve each include recursively with dig and tally every DNS query that involves an include, a, mx, redirect, or exists mechanism.
Solution: reducing SPF lookups
Remove unused include mechanisms
The fastest fix is to audit which services actually send email for your domain. Check your DMARC aggregate reports to see which source IPs are sending mail. If you decommissioned a marketing tool six months ago but left its include in your SPF record, remove it.
Common culprits: old CRM systems, decommissioned marketing platforms, trial accounts for email services you never activated, and includes added by former employees who no longer manage the domain.
Use subdomain delegation
Instead of cramming every service into the SPF record for your root domain, delegate specific services to subdomains. For example:
-
marketing.example.comfor your marketing ESP (Mailchimp, HubSpot) -
support.example.comfor your helpdesk (Zendesk, Freshdesk) -
notify.example.comfor transactional email (SendGrid, Amazon SES)
Each subdomain gets its own SPF record with only the includes it needs. The root domain's SPF record stays lean. Configure each service to use its designated subdomain as the envelope sender (Return-Path), and set up DMARC records on each subdomain as needed.
SPF flattening
SPF flattening replaces include mechanisms with the actual ip4 and ip6 addresses they resolve to. Since IP mechanisms do not count toward the 10-lookup limit, this can dramatically reduce your lookup count.
For example, instead of include:_spf.google.com (4 lookups), you would list the actual IP ranges that Google uses to send email.
Risks of flattening: IP addresses change. If Google, Microsoft, or any other provider adds, removes, or rotates IP ranges and you do not update your flattened record, legitimate mail will fail SPF. You must re-flatten regularly, ideally on an automated schedule.
For a detailed explanation of how flattening works and when to use it, see our SPF record flattening guide.
SPF softfail vs fail
Understanding ~all and -all
The all mechanism at the end of your SPF record defines what happens when a sending IP does not match any of the authorized mechanisms.
-
~all(tilde) = softfail. The IP is not authorized, but the domain owner is not confident enough to say "reject it." The message is accepted but marked. -
-all(hyphen) = fail. The IP is definitively not authorized. The receiving server may reject the message outright.
How softfail interacts with DMARC
This is one of the most misunderstood aspects of email authentication. SPF softfail (~all) passes the DMARC SPF check. DMARC only cares whether the SPF result is "pass" or "not pass." An SPF softfail result counts as "not pass," but DMARC evaluates the SPF authentication result separately from the SPF policy result.
More precisely: for DMARC purposes, SPF needs to return a "pass" result. Both ~all and -all will cause a "fail" or "softfail" result for unauthorized senders. Neither ~all nor -all will cause SPF to "pass" for an unauthorized sender. The DMARC SPF check fails in both cases for unauthorized senders, and passes in both cases for authorized senders. The difference between ~all and -all only matters for receivers that enforce SPF independently of DMARC.
When to use -all vs ~all
Use ~all when:
- You are still deploying DMARC and not sure you have identified all legitimate senders
- You rely on email forwarding (forwarded messages fail SPF because the forwarding server's IP is not in your SPF record)
- You want to be cautious during initial rollout
Use -all when:
- You have a complete inventory of all services that send email on your behalf
- Your DMARC policy is at
p=rejectand you have confirmed no legitimate mail is being blocked - You want maximum protection against spoofing for receivers that enforce SPF independently
SPF record syntax errors
Missing v=spf1 prefix
Every SPF record must begin with v=spf1 exactly. No space before it, no variation. If the prefix is missing or misspelled, the entire record is ignored.
# Wrong - missing prefix
"include:_spf.google.com ~all"
# Wrong - misspelled
"v=spf2 include:_spf.google.com ~all"
# Correct
"v=spf1 include:_spf.google.com ~all"
Multiple SPF records
RFC 7208 section 3.2 is explicit: a domain must not have more than one SPF record. If a DNS lookup returns two TXT records that both start with v=spf1, the result is a permerror.
This commonly happens when a new service provider tells you to "add this SPF record" and you create a new TXT record instead of modifying the existing one. Always merge new mechanisms into your single existing SPF record.
Check for duplicates:
dig TXT example.com +short | grep "v=spf1"
If you see more than one line of output, you have a problem.
The +all mistake
Using +all at the end of your SPF record means "every IP address on the internet is authorized to send email for my domain." This is almost never what you want. It effectively disables SPF entirely.
# Dangerous - authorizes everyone
v=spf1 include:_spf.google.com +all
# Correct
v=spf1 include:_spf.google.com ~all
If you see +all in a production SPF record, treat it as a security incident and fix it immediately.
The deprecated ptr mechanism
RFC 7208 section 5.5 explicitly discourages the use of the ptr mechanism. It requires the receiver to do a reverse DNS lookup for every sending IP, then a forward lookup on the result, which is slow and unreliable. Many receivers skip ptr evaluation entirely or treat it as a lookup failure.
If you find ptr in your SPF record, replace it with explicit ip4 or ip6 mechanisms for the same servers.
Other common typos
-
include _spf.google.com(missing colon after include) -
include:spf.google.com(wrong hostname, should be_spf.google.com) -
ip4: 192.168.1.0/24(space after the colon) -
v=spf1 ... all(bareallwithout a qualifier defaults to+all)
Use the DMARCTrust SPF generator to build a syntactically correct record from scratch, or validate an existing one with our domain checker.
Void lookups
What void lookups are
A void lookup occurs when a DNS query made during SPF evaluation returns either NXDOMAIN (the domain does not exist) or an empty answer (the domain exists but has no relevant records). RFC 7208 section 4.6.4 limits void lookups to 2 per SPF evaluation. If a third void lookup occurs, the result is permerror.
Why they happen
The most common cause is an include pointing to a domain that no longer publishes an SPF record. This can happen when:
- A service provider changes their SPF hostname and you still reference the old one
- A domain in your SPF record expires or is deleted
- An
aormxmechanism references a domain with no A/MX records
How to find void lookups
Resolve each domain referenced in your SPF record and check for NXDOMAIN or empty responses:
# Check if an included domain has an SPF record
dig TXT _spf.oldprovider.com +short
# If the response is empty or you get NXDOMAIN:
# ;; ->>HEADER<<- opcode: QUERY, status: NXDOMAIN
# That is a void lookup.
Walk through every include, a, mx, and exists mechanism in your SPF record (including nested ones) and verify each referenced domain resolves correctly.
Prevention
Monitor SPF with DMARC reports
DMARC aggregate reports tell you exactly which messages are failing SPF and why. If you are not already receiving reports, create a DMARCTrust account and publish a DMARC record with a rua tag pointing to your reporting address. Within 24 to 48 hours, you will start seeing data about every SPF evaluation for your domain.
Audit SPF records quarterly
Set a calendar reminder to review your SPF record every quarter. Check for:
- Includes for services you no longer use
- Total lookup count (stay at or below 10)
- Void lookups (domains that no longer resolve)
- Syntax correctness (one record, correct prefix, valid mechanisms)
Use the SPF generator
The DMARCTrust SPF generator lets you build an SPF record by selecting your email providers from a list. It calculates the lookup count automatically and warns you if you are approaching the limit.
Test changes safely
When making changes to your SPF record, follow this sequence:
- Use
~all(softfail) during testing so legitimate mail is not rejected - Publish the new record and wait at least 48 hours for DNS propagation and report collection
- Review your DMARC aggregate reports for unexpected SPF failures
- Once you are confident that all legitimate senders are covered, switch to
-all(fail)
This approach lets you catch mistakes before they impact mail delivery. If something goes wrong, switching back to ~all is a one-line DNS change.