Email and DNS
Fix SPF PermError: too many DNS lookups
On this page
You check your domain, or a DMARC report lands, and SPF says permerror. Often the reason is short: too many DNS lookups. It means your SPF record has grown past a hard limit in the standard, and receivers now treat it as broken. Not “a bit weak”: broken, for every message you send.
This guide explains the limit, shows you how to count your own lookups, and fixes it in the safest order: remove what you don’t need, move what doesn’t belong, and only then consider flattening. It finishes with checks that prove SPF passes again.
What the error means
SPF (Sender Policy Framework) is the TXT record starting v=spf1 that lists the servers allowed to send email for your domain. When a receiving server checks a message, it reads your record and follows any include: entries to other domains’ records, each of which needs another DNS query.
To stop one email check turning into hundreds of DNS queries, the SPF standard (RFC 7208, section 4.6.4) caps it: at most 10 lookups while evaluating one record, counting everything nested inside your includes. Go over and the receiver must return permerror, a permanent error.
A permerror is not a fail, but it isn’t a pass either. For DMARC that’s the same as SPF failing, so any message that relies on SPF for DMARC (rather than DKIM) can go to junk or be rejected once your DMARC policy is quarantine or reject. You’ll usually see it in one of these places:
- The headers of a message you sent, in the
Authentication-Resultsline:spf=permerror. - DMARC aggregate reports, where SPF shows as
permerrorfor your own servers. - An online checker that says “too many DNS lookups” or “SPF lookup limit exceeded”.
What counts as a lookup
Not every part of the record costs a lookup. These are the ones that do, and the ones that don’t:
| Part of the record | Counts? | Notes |
|---|---|---|
include: | Yes, 1 each | Plus everything inside the included record, so one include can cost several. |
a, mx | Yes, 1 each | Easy to forget. Often left over from a template and not needed. |
exists:, redirect= | Yes, 1 each | Some large providers use exists: in their own records. |
ptr | Yes | Slow and discouraged by the standard. Remove it. |
ip4:, ip6:, all | No | Free, which is why flattening works. |
There’s a second, lesser-known limit: void lookups. A lookup that finds nothing at all (an include: pointing at a domain with no SPF record, or that no longer exists) is a void lookup, and the standard says receivers should fail the check after two of them. Old includes for services that have shut down are the usual cause.
Two other mistakes give exactly the same permerror result, so rule them out while you’re here: having two SPF records on the same name (you must merge them into one), and a syntax error such as a space after include:.
Step 1: Count your lookups
Counting by hand means following every include down through every nested record, which is slow and easy to get wrong. Put your domain into the free email DNS checker instead. It follows the includes the way a receiving server does, gives the total against the limit of 10, lists how many lookups each part of your record costs, and flags void lookups and duplicate records.
Here’s a typical record that has grown over the years, with the breakdown you’d get. The service names are made up, but the pattern is common:
v=spf1 a mx include:spf.protection.outlook.com include:_spf.newsletter.example include:_spf.helpdesk.example include:_spf.invoices.example include:mail.oldcrm.example ~all
| Entry | Lookups |
|---|---|
a | 1 |
mx | 1 |
include:spf.protection.outlook.com (Microsoft 365) | 1 |
include:_spf.newsletter.example | 3 (it includes two more records) |
include:_spf.helpdesk.example | 2 |
include:_spf.invoices.example | 4 |
include:mail.oldcrm.example | 1 (void: the service closed and the record is gone) |
| Total | 13, over the limit |
Write your own breakdown down. You’ll use it to decide what to cut.
Microsoft 365’s include:spf.protection.outlook.com and Google Workspace’s include:_spf.google.com each cost just 1 lookup when this guide was written (November 2026), because both records now list their IP ranges directly. The expensive ones are usually the smaller services.
Step 2: Remove what you don’t need
This fixes most records on its own, and it carries no risk to mail that’s actually being sent. Go through the breakdown line by line:
- Services you’ve stopped using. Old CRMs, old newsletter tools, a previous email host. If nobody can say what an include is for, look at your DMARC reports: if no mail has come from that service in the last month or two, it can go. Void includes go first.
aandmx.aauthorises your website’s server;mxauthorises the servers that receive your mail. If your website doesn’t send email itself and you use Microsoft 365 or Google Workspace (whose include already covers sending), neither is needed. That’s two lookups back straight away.ptr. Always remove it.- Duplicates. The same service included twice under different names, or an include that’s already inside another include.
In the example, removing a, mx and the dead CRM include takes the total from 13 to 10. That would work, but with no room to spare: the next service someone signs up for, or a provider adding a nested include to its own record, would break it again. Aim for 8 or fewer.
Step 3: Move services off your main record
This is the step most guides miss. SPF doesn’t check the From address people see. It checks the envelope sender, also called the Return-Path or bounce address, which the receiving server records in the message headers. Many sending services use their own domain, or a subdomain of yours, for that address. When they do, the include in your main record isn’t being used for those messages at all.
To check a service, send yourself a message through it, open the full headers (in Outlook, File > Properties; in Gmail, Show original) and find the Return-Path line:
- Return-Path on the service’s own domain (such as
bounces@mail.service.example): SPF is checked against the service’s record, not yours. Remove the include from your record. Make sure the service signs with DKIM for your domain, because that’s what will pass DMARC for these messages. - Return-Path on a subdomain of yours (such as
bounce.example.co.uk): the SPF record that matters is on that subdomain. Put the service’s include there instead of on your main domain. With DMARC’s default relaxed alignment, a pass onbounce.example.co.ukstill counts forexample.co.uk. - Return-Path on your main domain: the include has to stay in your main record, unless the service offers a “custom return path” or “custom bounce domain” setting. Most bulk email services do; setting one up moves the service onto a subdomain as above.
Moving one or two busy services onto their own subdomains is the cleanest long-term fix, because each subdomain gets its own allowance of 10 lookups.
Step 4: Flatten only what you must
Flattening means replacing an include: with the IP addresses it currently contains, written as ip4: and ip6: entries, which cost no lookups. You can see what an include contains by looking up its TXT record in the DNS lookup tool.
It works, but it has a serious catch: when the service changes its sending IP addresses, your copy doesn’t change with it, and mail from the new addresses fails SPF without anyone noticing until reports or complaints arrive. So:
- Never flatten Microsoft 365 or Google Workspace. They cost 1 lookup each anyway, and their ranges do change.
- Flatten only addresses you control or that rarely change, such as your own office mail server or a scanner relay with a fixed IP.
- Use an automatic flattening service if you must flatten a third party. These re-check the provider’s record and update your DNS for you. Doing it by hand needs a calendar reminder and discipline.
- Keep the record short. Each DNS text string is limited to 255 characters. Most DNS panels split long values for you, but a very long record is harder to read, easier to break, and can cause problems for some receivers.
After step 2 and step 3, most organisations don’t need to flatten anything.
Step 5: Publish and prove it works
Edit the existing SPF record rather than adding a new one. After the clean-up, the example looks like this, with the newsletter service moved to its own bounce subdomain:
v=spf1 include:spf.protection.outlook.com include:_spf.helpdesk.example include:_spf.invoices.example ~all
That’s 1 + 2 + 4 = 7 lookups. Then check it in three ways:
- Run the email DNS checker again. You want one SPF record, a lookup count under 10 (ideally 8 or fewer) and no void lookups. If the old record still shows, wait for the TTL (the cache time on the record, often an hour) and try again.
- Send a test message from each service to a Gmail or Outlook.com address, open the original message and check the Authentication-Results line shows
spf=passanddmarc=pass. - Watch your DMARC reports for a week. Drop them into the DMARC report analyser: the permerrors should stop, and every one of your own services should pass SPF or DKIM with alignment.
~all or -all?
Ending with ~all (soft fail) or -all (fail) doesn’t change the lookup count. Once you have DMARC at p=reject, DMARC decides what happens to failing mail, and ~all is a reasonable choice that avoids some forwarding problems. Don’t change it as part of this fix unless you have a reason to.
Quick checklist
- One SPF record only, starting
v=spf1 - Count lookups, including nested includes: 10 is the hard limit, 8 or fewer is comfortable
- No void lookups (includes that find nothing)
- Remove old services,
ptr, andaandmxif they don’t send - Check each service’s Return-Path and move it to a subdomain or drop its include where possible
- Flatten only stable addresses, never Microsoft 365 or Google
- Test with real messages and watch DMARC reports for a week