← Back to Email Authentication Checker

SPF PermError: fixing “too many DNS lookups”

An SPF record is allowed 10 DNS lookups. Go over, and RFC 7208 §4.6.4 says the receiver must return PermError — for every message you send, permanently, until the record is shortened. Nothing warns you. There is no bounce that says “your SPF record is too long”.

This is the most common real-world SPF bug, and it has a boring cause: you add one include: for your mail host, another for your invoicing tool, another for your helpdesk, another for the newsletter — and one day the total quietly crosses 10.

What actually breaks (and why you might not notice)

SPF evaluation stops at the limit and returns PermError. Receivers read that as “this domain’s SPF cannot be evaluated”, so the SPF half of DMARC alignment fails. If your DKIM signature is present and aligned, DMARC can still pass on DKIM alone — which is exactly why a domain over the limit often keeps delivering and the bug survives for months. You lose the redundancy silently, and find out on the day DKIM breaks or a receiver weighs SPF on its own.

Which mechanisms cost a lookup

TermCosts a lookup?Notes
include:Yes — 1Plus everything inside the record it pulls in
redirect=Yes — 1Plus everything inside the record it points at
a / a:Yes — 1One per mechanism
mx / mx:Yes — 1Capped separately at 10 MX records returned
exists:Yes — 1One per mechanism
ptrYes — 1§5.5 says do not publish it — see below
ip4: / ip6:NoAddress literals; no DNS query needed
allNoMatches without a query
exp=NoFetched only when a message fails, outside the budget

The budget is cumulative across the whole chain. A lookup inside a record you include: spends from the same 10 as one in your own record. This is what makes the bug hard to see by eye: a record with four include: terms looks like four lookups and can easily be eighteen.

The 10 are beyond the initial TXT query that fetches your record, so reading your own SPF does not spend from the budget.

Count yours

The Email Authentication Checker walks every include: and redirect= recursively and reports the chain total against the limit, naming the exact record each lookup came from. In the browser, or from a terminal:

npx --yes github:Duncan-Haywood/monorepo-projects#main \
  --package-lock=false ventures/email-auth-check/cli.mjs example.com

Or clone the repository and run node ventures/email-auth-check/cli.mjs example.com. Both query public DNS-over-HTTPS directly from wherever you run them; nothing is sent to us.

Six ways back under the limit

1. Delete includes for tools you no longer send from

The cheapest fix and usually the largest. Every SaaS trial that once asked you to add an include: is still costing you a lookup. Go through the list and ask, per entry, “does mail still leave from this?”

2. Replace a and mx with the addresses they resolve to

If your mail server has a stable, static IP, ip4:203.0.113.10 does the same job as mx for zero lookups. This is safe precisely where flattening an ESP is not: it is your address, and you control when it changes.

3. Remove ptr

RFC 7208 §5.5 says ptr SHOULD NOT be published and that receivers may ignore it. It spends a lookup, it is slow, and its answer is not dependable. Deleting it is nearly always free.

4. Move bulk mail to a subdomain

Send newsletters from news.example.com and transactional mail from mail.example.com. Each subdomain gets its own 10-lookup budget and its own SPF record, so the ESP includes move off the parent domain entirely. This is the fix that keeps working as you add tools, rather than buying you two or three more.

5. Consolidate overlapping vendors

Two tools that both send “your receipt” email are two includes. Sometimes the honest fix is fewer tools.

6. Flatten — knowing what it costs

Flattening replaces an include: with the ip4:/ip6: ranges it resolves to today. It genuinely removes lookups. It also freezes your provider’s IP list at the moment you flattened it: when they add or move a range, your record still authorizes the old one, and mail starts failing SPF with no error anywhere to explain it. That is strictly worse than the problem you started with, because PermError is at least visible.

Flatten only if something re-flattens on a schedule and alerts you when the upstream record changes. If that is not in place, prefer fix 4.

A worked example

v=spf1 include:_spf.google.com include:sendgrid.net include:servers.mcsv.net mx a ~all

Four visible lookup terms plus mx and a — six, by eye. Walk into them and _spf.google.com alone contributes several through its own nested includes. Chains shaped like this routinely total 12–18. The fix here is usually (4): give the marketing tool its own subdomain, leaving the parent record with the mail host and a couple of ip4: literals.

After you change the record

Common questions

What does an SPF PermError from too many DNS lookups actually break?

SPF evaluation stops and returns PermError for every message from the domain, not just some of them. Receivers treat PermError as "this domain's SPF cannot be evaluated", so the SPF half of DMARC alignment fails. If DKIM is signing and aligned, DMARC can still pass on DKIM alone — which is why a domain over the limit often keeps delivering and the bug goes unnoticed for months.

Which SPF mechanisms count toward the 10-lookup limit?

RFC 7208 section 4.6.4 counts the include, a, mx, ptr and exists mechanisms, plus the redirect modifier. Each one costs exactly one lookup toward the budget. The ip4, ip6 and all mechanisms cost nothing because they need no DNS query, and the exp modifier is only fetched when a message fails, so it is outside the budget too.

Is the limit 10 lookups or 11?

The budget is 10 lookups beyond the initial TXT query that fetches the record itself, so fetching your own SPF record does not spend from the 10. The count is also cumulative across the whole chain: a lookup inside a record you include: spends from the same budget as one in your own record.

Does SPF flattening fix it permanently?

No. Flattening replaces an include: with the ip4/ip6 ranges it resolves to today, which does remove lookups — but it also freezes your sending provider's IP list at the moment you flattened it. When that provider adds or moves a range, your record still authorizes the old one and your mail starts failing SPF with no error anywhere to explain it. Flattening trades a visible failure now for a silent one later unless something re-flattens it on a schedule.

Should I use the ptr mechanism in SPF?

No. RFC 7208 section 5.5 says ptr SHOULD NOT be published, and that receivers may ignore it entirely. It costs a lookup from your budget, it is slow, and its result is not dependable. If a record you inherited contains ptr, removing it is almost always both safe and a free lookup back.

Why does my SPF record pass in one checker and fail in another?

Most commonly because the other checker counted only the mechanisms in your own record rather than walking into each include: and counting what is inside it. The limit applies to the whole recursive chain, so a record with four include: terms can be well over 10 while looking like four. A checker that reports a per-record count rather than a chain total will disagree with what a receiver computes.