An SPF record fails with a permanent error when evaluating it needs more than 10 DNS lookups, and the message may be rejected with "The message exceeded the hop count" or "The message required too many lookups". To fix it, count the lookups your record really causes, delete includes for services you no longer use, move third-party senders to subdomains that have their own SPF record, and replace an include with ip4: ranges only for vendors whose addresses are stable. Do not flatten include:spf.protection.outlook.com.
Who this is for and what you will have at the end
This guide is for administrators who use Microsoft 365 for mail from a custom domain and have added marketing tools, ticketing systems, CRMs or on-premises servers to the same SPF record, and who now see SPF permerrors or bounces.
At the end you will have:
- A clear picture of what counts toward the limit and what does not.
- A method to count lookups, including nested ones.
- A ranked list of fixes, from least to most risky.
- A corrected record, and a way to verify it.
What the 10-lookup limit is
SPF is defined in RFC 7208. Section 4.6.4 requires that implementations cap the counted terms in an SPF evaluation at 10, and that exceeding the cap produces a permerror. Per section 2.6.7, a permerror means the published records could not be correctly interpreted and DNS operator intervention is needed.
The terms that count are:
| Counts toward 10 | Does not count |
|---|---|
include: | ip4: |
a | ip6: |
mx | all |
ptr | |
exists | |
redirect modifier |
Microsoft's own page lists include:, a, mx, exists and redirect as counting, and ip4:, ip6: and all as not counting. The RFC also counts ptr, so use the RFC as the authority. The RFC adds two details:
- Each MX record queried counts toward the 10, and evaluating one MX record must not query more than 10 address records, or the
mxmechanism returns permerror. - Implementations should limit void lookups (answers that are empty or NXDOMAIN) to 2. Exceeding it produces a permerror, so a stale
include:pointing to a domain that no longer has an SPF record can cause trouble on its own.
Nested includes are the usual culprit
Each include: costs at least one lookup, and the record it points to is evaluated too. Microsoft's example: an include: that points to another SPF record containing three more include: statements adds four lookups in total. So having fewer than 10 include: statements does not guarantee fewer than 10 lookups.
Why it fails only sometimes
Receivers evaluate sources from left to right and stop when the sending source matches. An SPF record that could cause more than 10 lookups can still pass for sources listed early, and fail for sources listed late. Microsoft notes that some destinations may not go deep enough in the record to hit the error. That is why the symptom looks random: one sender's mail works, another's bounces.
Prerequisites
- Access to the DNS host for your domain. Microsoft 365 has no admin portal or PowerShell cmdlet for managing SPF records, so you edit the TXT record at your registrar or DNS hosting service.
- A list of every service that sends mail as your domain. If you do not have one, start with the bounce messages, message headers and, if you have it, DMARC reports. Microsoft recommends a DMARC reporting service for identifying email sources and SPF failures.
- A copy of the current SPF record, so you can roll back.
Step 1: Read the current record
Query the TXT record for your domain. On Windows PowerShell:
Resolve-DnsName -Name contoso.com -Type TXT | Where-Object { $_.Strings -match '^v=spf1' } | Select-Object -ExpandProperty StringsOn Linux or macOS:
dig +short TXT contoso.comConfirm there is exactly one record that starts with v=spf1. Multiple SPF TXT records for the same domain cause SPF to return permerror, because the receiver cannot decide which to evaluate. If you find two, merge them into one.
A typical over-grown record looks like this (the includes are placeholders):
v=spf1 include:spf.protection.outlook.com include:mail.crm.example include:send.marketing.example include:tickets.support.example include:old.newsletter.example mx a ip4:203.0.113.10 -allStep 2: Count the lookups
Walk the record and count each counted term, then open every include: target and count the terms inside it, and so on. Microsoft's page says some free online tools show your SPF record and even calculate the lookups required. Use one to confirm your manual count, but check the result against the rules above because tools differ on ptr and void lookups.
A worksheet helps:
| Term | Lookups at this level | Nested lookups | Total |
|---|---|---|---|
include:spf.protection.outlook.com | 1 | Count what its record contains | |
include:mail.crm.example | 1 | Count | |
mx | 1 (plus each MX record queried) | ||
a | 1 | ||
ip4:203.0.113.10 | 0 | 0 |
If the total is above 10, continue.
Step 3: Apply the fixes in order
Fix 1: Remove what you no longer use
Delete includes for services that no longer send mail as your domain. Old newsletter tools, trial products and decommissioned ticketing systems are the common ones. This costs nothing and cannot break current mail, provided your list of senders is accurate.
Also check for includes that no longer resolve. Microsoft warns that if an include: domain does not resolve or has no SPF record, SPF validation returns permerror and messages might be rejected. Verify each include by querying the TXT record of the referenced domain.
Fix 2: Remove mx and a if they are not needed
Each costs a lookup, and mx costs one for each MX record it queries. If your mail is sent only by Microsoft 365 and listed services, you probably do not need them. Check first, because an on-premises server that sends mail may rely on them. If you do need an on-premises server, it is usually cheaper to list its public address with ip4:, which does not count.
Fix 3: Move third-party services to subdomains
Microsoft recommends using a subdomain such as marketing.contoso.com for email services that you do not directly control, so problems with those senders do not affect the reputation of your main domain. It is also the best fix for the lookup limit, because each subdomain has its own SPF record and its own budget of 10 lookups.
The steps are:
- Choose a subdomain for the service.
- Configure the service to send from that subdomain, following its documentation.
- Publish an SPF TXT record for the subdomain.
- Remove the service's include from the main domain's record.
Following Microsoft's example for a bulk mailing service:
contoso.com TXT "v=spf1 ip4:192.0.2.10 include:spf.protection.outlook.com -all"
marketing.contoso.com TXT "v=spf1 include:servers.adatum.com include:spf.protection.outlook.com -all"Remember that the record for contoso.com does not cover marketing.contoso.com, so every subdomain that sends mail needs its own SPF TXT record. Only include spf.protection.outlook.com in the subdomain's record if that subdomain also sends from Microsoft 365.
Fix 4: Consolidate services
If two tools do the same job, retire one. Fewer vendors means fewer includes and fewer things to monitor.
Fix 5: Replace an include with IP ranges (flattening), carefully
SPF flattening replaces include: with the resolved IP addresses, which cost nothing because ip4: and ip6: do not count. Microsoft's guidance:
- Appropriate for vendors with stable, documented IP ranges, services that rarely change addresses, and when you are at or over the limit and the other fixes are not feasible.
- Do not flatten Microsoft 365 (
include:spf.protection.outlook.com), because its sending infrastructure uses dynamic IP addresses that change frequently. Also do not flatten any cloud service with dynamic or frequently changing addresses. - If you flatten, document what you replaced and when, monitor vendor documentation for address changes, review the flattened entries at least quarterly and test SPF after each change.
Non-Microsoft flattening services can automate tracking vendor changes if manual maintenance is impractical. Evaluate them against your own risk tolerance, because they place a third party in control of part of your DNS.
Step 4: Check the syntax
A record can fail even with few lookups if it is malformed. Microsoft lists these common mistakes:
| Incorrect | Problem | Correct |
|---|---|---|
v=spf1 include:spf.protection.outlook.com. -all | Trailing period after the domain | v=spf1 include:spf.protection.outlook.com -all |
v=spf1 include=spf.protection.outlook.com -all | Equals sign instead of colon | v=spf1 include:spf.protection.outlook.com -all |
v=spf1 include: spf.protection.outlook.com -all | Space after the colon | v=spf1 include:spf.protection.outlook.com -all |
The minimum Microsoft 365 record for a domain that sends only from Microsoft 365 is:
v=spf1 include:spf.protection.outlook.com -allMicrosoft recommends -all (hard fail) for Microsoft 365 domains when you also use DKIM and DMARC. With ~all (soft fail), the DMARC policy is effectively ignored for SPF failures if the message has no DKIM signature. Do not use ?all in production. The GCC High and DoD clouds and Microsoft 365 operated by 21Vianet use different include values, which are listed in Microsoft's SPF page.
Microsoft also recommends a TTL of at least 3600 seconds on SPF TXT records to avoid DNS lookup timeouts.
Step 5: Publish and verify
- Lower your change risk by editing the record outside your busiest sending hours.
- Replace the existing record rather than adding a second one.
- Wait for the TTL of the old record to expire.
- Query the record again with the command from Step 1 and recount the lookups.
- Send a test message from each sender, including each third-party service, to a mailbox you can inspect, and check the
Authentication-Resultsheader forspf=pass. - Watch DMARC reports and bounces for a week for senders you missed.
If you have not yet set up DKIM and DMARC, read SPF, DKIM and DMARC setup for Microsoft 365 and fixing a missing DKIM CNAME in Microsoft 365. SPF alone is not enough for protection against spoofing.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Bounce says the message exceeded the hop count or required too many lookups | More than 10 lookups | Work through the fixes above |
| Permerror but fewer than 10 includes | Nested lookups or mx with many records | Recount including nested records |
| One sender fails and others pass | Failing source is deep in the record | Reorder or reduce, and recount |
| Permerror after adding a service | Second SPF TXT record published | Merge into one record |
| Permerror with a short record | Typo or include without an SPF record | Check the syntax table and query each include |
| Works at some receivers, not others | Receivers evaluate to different depths | Reduce lookups, not just reorder |
| Subdomain mail fails SPF | No SPF record for that subdomain | Publish a record for it |
Checklist
- One SPF TXT record per domain and per sending subdomain.
- A documented list of every sender, with an owner for each.
- Ten lookups or fewer, counted including nested includes.
- Third-party senders on subdomains, not the main domain.
- No flattening of
include:spf.protection.outlook.com. -allat the end, with DKIM and DMARC in place.- A quarterly review of any flattened ranges.
Comments
Questions and tips are welcome. Comments are checked by the EpiFive team before they appear; please do not share passwords, phone numbers or other private details.