All posts

Fixing 'SPF PermError: Too Many DNS Lookups', Worked Example

··7 min read

SPF permits at most 10 DNS-querying terms per evaluation. Every include, a, mx, ptr, exists and redirect counts, including those nested inside included records. Exceed 10 and receivers return permerror, which DMARC treats as a failure. Fix it by removing includes you don't need, moving senders to subdomains, or replacing hostnames with IP ranges.

The rule is in RFC 7208, section 4.6.4: "SPF implementations MUST limit the total number of those terms to 10 during SPF evaluation... If this limit is exceeded, the implementation MUST return 'permerror'." Most guides stop there. The useful part is knowing what each include actually costs, so we measured it.

What common includes cost

We counted the lookups behind each provider's published SPF include on 1 October 2026, using dig and a small script that walks the include tree. The cost is the include itself plus every lookup-causing term beneath it. These change whenever a provider edits its records, so re-check before relying on a number.

Include Lookups it costs Why
_spf.google.com (Google Workspace) 1 Flat list of IP ranges
spf.protection.outlook.com (Microsoft 365) 1 Flat
servers.mcsv.net (Mailchimp) 1 Flat
amazonses.com (Amazon SES) 1 Flat
mail.zendesk.com 1 Flat
spf.mandrillapp.com 1 Flat
spf.brevo.com 1 Flat
spf.mtasv.net (Postmark) 1 Flat
mktomail.com (Marketo) 1 Flat
sendgrid.net 2 Nests ab.sendgrid.net
_spf.salesforce.com 2 Uses an exists: macro
sparkpostmail.com 2 One nested include
zohomail.com 2 One nested include
mailgun.org 5 Four nested includes
zoho.com 5 Four nested includes

Most single-provider includes cost one lookup. The expensive ones are providers that nest several levels deep. Two of those in one record use 10 of your budget between them.

A real record over the limit

Counting the SPF record for gitlab.com on 1 October 2026 gives 13. We chose it because it is a public record from a well-run company that shows exactly how records drift over the limit: one vendor at a time. Here is the top-level record, with the running count in order of evaluation:

v=spf1 include:mail.zendesk.com      # 1
       include:_spf.google.com       # 2
       include:mktomail.com          # 3
       include:_spf.salesforce.com   # 4, plus exists: inside = 5
       include:_spf-ip.gitlab.com    # 6
       a:zgateway.zuora.com          # 7
       include:mailgun.org           # 8
         -> include:_spf.mailgun.org       # 9
              -> include:_spf1.mailgun.org # 10
              -> include:_spf2.mailgun.org # 11
         -> include:_spf.eu.mailgun.org    # 12
       include:_spf.sendergen.com    # 13
       ip4:35.80.141.6/32 ip4:44.229.121.55/32 -all

Nothing here is unusual. Each line is a legitimate vendor, and the overrun comes almost entirely from mailgun.org, which on its own costs five.

Why it doesn't fail all the time

The limit is applied during evaluation, and SPF evaluates left to right, stopping at the first match. Following the RFC strictly:

  • Mail sent from Google's IP ranges matches at lookup 2 and passes. The limit is never reached.
  • Mail from Mailgun's first block of ranges (_spf1) matches at lookup 10 and passes, just.
  • Mail from Mailgun's second block (_spf2) needs lookup 11, so the receiver returns permerror.
  • Mail from the last include never gets evaluated at all.

So the same record passes for some senders and fails for others. That is why "too many lookups" problems often present as one tool's mail failing DMARC while everything else looks fine. A static checker that counts the whole tree, like the script below, reports 13 whichever server sent the message. Either way, you can't rely on a record over 10.

Count your own record

Run this anywhere with Python 3 and dig. It prints the tree and the total.

import subprocess, re, sys

def spf(name):
    out = subprocess.run(["dig", "+short", "TXT", name], capture_output=True, text=True).stdout
    recs = ["".join(re.findall(r'"((?:[^"\\]|\\.)*)"', l)) for l in out.splitlines()]
    return [r for r in recs if r.lower().startswith("v=spf1")]

def count(name, depth=0):
    recs = spf(name)
    print("  " * depth + name + ": " + (recs[0] if recs else "NO SPF RECORD"))
    if not recs:
        return 0
    total = 0
    for term in recs[0].split()[1:]:
        t = term.lstrip("+-~?").lower()
        if t.startswith("include:"):
            total += 1 + count(t.split(":", 1)[1], depth + 1)
        elif t.startswith("redirect="):
            total += 1 + count(t.split("=", 1)[1], depth + 1)
        elif t in ("a", "mx", "ptr") or t.startswith(("a:", "a/", "mx:", "mx/", "ptr:", "exists:")):
            total += 1
    return total

print("TOTAL:", count(sys.argv[1]))

python3 spf_count.py yourdomain.com. Or use our SPF checker, which does the same count without the terminal.

Two other limits in the same RFC section catch people out:

  • Void lookups. RFC 7208 says implementations "SHOULD limit 'void lookups' to two", meaning lookups that return no records or a non-existent domain. An include pointing at a vendor you stopped using, whose record has since been deleted, uses up one of these.
  • The mx mechanism. Each mx term counts as one lookup, and the MX hosts it returns may not resolve to more than 10 addresses. ptr is marked "do not use" in the RFC.

And one that isn't a lookup limit but produces the same permerror: publishing two separate SPF records on one domain. Merge them into one.

How to get back under 10

Work through these in order. The first two fix most records without any ongoing maintenance.

1. Remove includes that do nothing for you

SPF checks the domain in the envelope sender (the Return-Path, or MAIL FROM), not the From address your recipients see. Many email platforms put their own bounce domain in the envelope sender by default. If yours does, receivers check SPF against the platform's domain, and the include in your record is never consulted.

To check, send yourself a message through each platform and look at the Return-Path header:

Return-Path shows... Do you need the include?
bounce@yourdomain.com or a subdomain of yours Yes, SPF is checked against your domain
bounces+123@em.platform.com or similar No, remove it. DMARC alignment comes from DKIM instead

That second case only works if the platform signs DKIM with your domain, so confirm that with our DKIM checker before deleting anything. Also delete includes for tools you no longer use. Old includes are the most common source of void lookups.

2. Move senders to subdomains

Each subdomain gets its own SPF record and its own budget of 10. Put marketing on news.yourdomain.com and support on help.yourdomain.com, configure the platform to use that subdomain as its custom return-path domain, and your root record only needs to cover your mailboxes. DMARC alignment still works, because relaxed alignment (the default) treats subdomains of the same organisational domain as aligned.

3. Replace hostname lookups with IP ranges you control

An a: or mx that points at your own server costs a lookup every time. If the IP is static and yours, ip4:203.0.113.10 costs nothing.

4. Flatten, carefully

SPF flattening replaces includes with the IPs they currently resolve to. It reliably gets you under the limit and reliably goes stale, because providers change their ranges without telling you. If you flatten, do it with a service or a scheduled script that re-resolves and republishes, and don't flatten providers like Salesforce whose record uses an exists: macro. That isn't a list of IPs, so there is nothing to flatten.

A fix, start to finish

Here is a hypothetical company using the costs measured in the table above. Its mailboxes are on Google Workspace, marketing goes through SendGrid, product notifications through Mailgun, and sales and support use Salesforce and Zendesk:

v=spf1 include:_spf.google.com include:sendgrid.net include:mailgun.org
       include:_spf.salesforce.com include:mail.zendesk.com ~all
Include Cost
_spf.google.com 1
sendgrid.net 2
mailgun.org 5
_spf.salesforce.com 2
mail.zendesk.com 1
Total 11

One over. Two changes bring it down without flattening anything:

  1. Product notifications move to notify.example.com, with Mailgun configured to use that subdomain in the envelope sender. The subdomain gets its own record, v=spf1 include:mailgun.org ~all, costing 5 of its own 10.
  2. The root record drops include:mailgun.org, and falls to 6.

That leaves four lookups of headroom on the root domain for the next tool someone signs up for, and keeps Mailgun's nested includes somewhere they can't push anything else over the limit.

After the fix

Re-count, then send a test through every platform you use and check Authentication-Results for spf=pass. Remember that SPF is one of three checks. DMARC passes if either SPF or DKIM passes and aligns, so a clean DKIM setup is what keeps a temporary SPF problem from becoming a delivery problem. Our SPF generator builds a fresh record if you would rather start again than edit the old one.

The record you have now was built one vendor at a time. Count it whenever you add one.

Common questions

Which SPF mechanisms count towards the 10 DNS lookup limit?

Under RFC 7208, the include, a, mx, ptr and exists mechanisms and the redirect modifier each count as one lookup, and lookups inside included records count too. The ip4, ip6 and all mechanisms do not count, which is why replacing hostnames with IP ranges reduces the total.

What happens when an SPF record exceeds 10 lookups?

The receiving server must return a permerror result instead of pass or fail. Because the limit is applied during evaluation, mail that matches an early mechanism can still pass while mail from a sender listed later fails, which makes the problem look intermittent.

Is SPF flattening a good fix?

It works but it is fragile. Flattening replaces includes with the IP ranges they currently resolve to, so when a provider changes its IPs your record silently goes stale. Use it only with a service or script that re-flattens automatically, and try removing unnecessary includes first.

Do I need an include for every email platform I use?

Only for platforms that send with your domain in the envelope sender (Return-Path). Many platforms use their own bounce domain there, in which case SPF is checked against their domain, not yours, and your include does nothing. Those platforms rely on DKIM for DMARC alignment.

Verify unlimited addresses for $29.99/month

Real SMTP mailbox checks. No credits, no per-email fees.

Get Started