SPF PermError: too many DNS lookups, and how to get back under ten
“SPF PermError: too many DNS lookups” means the receiving server started walking your SPF record, hit the eleventh DNS query, and stopped. It didn’t score the part it had already checked. RFC 7208 section 4.6.4 tells a receiver to return PermError at that point, and PermError voids the whole record, so for as long as the error stands your domain has no working SPF, whatever the text in your DNS panel says.
That’s the code. The cause is nearly always a budget spent by other people’s records: each include: you add pulls in whatever the provider put inside it, charged recursively, so four visible lines can resolve to fourteen lookups, and a record can tip over with no edit in your own zone. The way back starts with counting the real total and pruning what’s dead. Flattening can wait until those are done.
What the receiver did when it gave up
You’ll usually meet the string in an Authentication-Results header on a bounced or quarantined message, in a form like this:
Authentication-Results: mx.receiver.example;
spf=permerror (too many DNS lookups) smtp.mailfrom=yourdomain.example
An SPF check is a chain of DNS queries. The receiver fetches your TXT record, then follows each mechanism that needs another lookup: an include: fetches another record, an a fetches an address, an mx fetches the mail hosts and then their addresses. RFC 7208 caps that whole walk at ten lookups. On the eleventh it returns PermError and discards the evaluation.
The includes that fit under the cap don’t get scored on their own. The receiver treats the whole record as unusable, so a message from a sender you listed correctly on line one gets the same result as a message from a sender you never listed at all.
The same section of the RFC sets a second, less known ceiling: at most two void lookups, meaning queries that return no answer or NXDOMAIN. Two include: entries pointing at services you cancelled years ago are enough to void the record on their own, before the ten-lookup count even comes into it.
DMARC makes this worse. A DMARC evaluation treats an SPF PermError as an SPF fail, so on the SPF leg your mail is unauthenticated until the record evaluates again, and delivery leans entirely on DKIM. If a message’s DKIM signature is missing, or a mailing list rewrites the body in transit, that message fails DMARC outright. A strict -all on the end changes nothing here, because the qualifier only applies to a record that evaluates.
Why it broke when you changed nothing
The lookup budget belongs to you, and most of it is spent by records you don’t control. When you write include:_spf.crm-vendor.example, the receiver fetches that vendor’s record and charges you for each lookup mechanism inside it, and inside anything that record includes in turn. One line in your panel can cost four or five lookups once resolved.
So the record that worked in March can return PermError in September because a provider nested one more include inside theirs. Hosting-panel include chains several layers deep are a common source. So is ordinary SaaS stacking: a mailbox provider, a newsletter tool, a CRM and a helpdesk. Each vendor asks you for a single include that costs several lookups once expanded, and four sensible additions put the total past ten without anyone deciding to.
Of the 150,103,209 domains publishing an SPF record in the September 2026 census, the edition flagged 7,408 for too many lookups, and that count is a floor because the census reads the declared record rather than resolving each include chain, so a record that only overflows once its includes expand doesn’t appear in it. The figure tells you the failure is real at internet scale, and nothing about whether your own record is one of the unresolved ones.
What each mechanism costs
Count against this table. The cost is per occurrence, and the total is for the whole evaluation, top to bottom, with includes expanded.
| Mechanism | Lookups | Note |
|---|---|---|
include: | 1 plus everything inside it, recursively | where nearly all blowouts start |
a | 1 each | even a bare a for your own domain |
mx | 1 each, plus the MX hosts’ A lookups | often forgotten |
ptr | 1 | deprecated since 2014, delete it regardless |
exists: | 1 each | rare |
redirect= | 1 plus the target record’s contents | counts like an include |
ip4: and ip6: | 0 | this is why flattening works |
-all, ~all, ?all | 0 | the qualifier is free |
A record that reads v=spf1 include:a.example include:b.example include:c.example include:d.example -all looks like four lookups. If the first include carries two includes of its own, the second carries three, and one of those carries an mx, you’re at fourteen before the receiver gets anywhere near the -all. Count the resolved total; the visible lines tell you nothing on their own.
Resolve the chain before you edit anything
You can’t count this by eye, and a DNS panel won’t do it for you either. The panel shows the record you published; the receiver evaluates the tree that record expands into at the moment a message arrives. What you need is a resolved count done the way a receiver does it, with a list of which include cost what.
The free scan does that walk for your domain, shows the resolved lookup total and lists each include with its cost, alongside your DKIM and DMARC state. Run it before you edit, so you’re working from the chain as it resolves today.
If the scan reports no SPF at all, that’s a different problem: a record that hasn’t been published, or was published on the wrong name. The SPF page covers that case, and the steps below assume you have a record that resolves to more than ten.
The fix, cheapest step first
The first two steps rescue most over-budget records, and the last is the one that goes wrong later, so take them in this order.
Delete dead and unused includes. The tool you dropped a couple of years ago, the old host’s include that survived a migration, a duplicate line, and any ptr mechanism. Dead includes cost twice: they spend lookup budget, and they’re the usual source of void lookups. Most records get back under ten here and need nothing else.
Check that each remaining include earns its place. Receivers evaluate SPF against the Return-Path domain (RFC5321.MailFrom); the From header plays no part in it. Many SaaS tools put their own bounce domain on the Return-Path, so their include in your record spends budget and authorises nothing. Keep an include only where the service sends with your domain as the Return-Path. For the rest, switch on the vendor’s custom-domain DKIM signing and take the include out.
Move bulk senders onto their own subdomains. Newsletters from news.yourdomain.example, transactional mail from mail.yourdomain.example. Each subdomain has its own SPF record and its own fresh ten-lookup budget, so a blowout in one stream stops bleeding into the others, and your root record shrinks back to the mailbox provider plus whatever else sends as you.
Only after those three should flattening come up, and only in the automated form.
Flattening, and why a person shouldn’t do it by hand
Flattening replaces includes with the ip4: and ip6: blocks they resolve to. IP mechanisms cost zero lookups, so the count collapses. Done once by hand, it’s a delayed delivery outage. Providers rotate their sending addresses without telling you, so a pasted list is correct on the day and out of date within months. The failure shows up as other people not receiving your mail, with no error on your side, which is why you find out late.
Automated flattening is different. A service or a scheduled job that re-resolves the includes and republishes the record tracks the providers’ changes for you, and that’s safe to run. If pruning and subdomains get you under the limit, stop there. If they don’t, flatten with something that re-runs on a schedule, and don’t leave a hand-maintained IP list in your zone, because it goes stale the next time a provider changes addresses.
Leave headroom, then check again
If the new count is exactly ten, the next include puts you straight back into PermError. Aim for a resolved count with room in it, so the next tool your marketing team signs up for doesn’t switch the record off again. Re-run the scan after the change and confirm the evaluation now returns pass with a resolved count you can live with. Note the figure somewhere the next person in your seat will find it.
Figures as of 5 September 2026, from the September 2026 edition of the defaults.exposed census. Census numbers move every month; the current values are on the census data page.