NetSendo
Guides & Tips

SPF PermError "too many DNS lookups": fix the 10-lookup limit

NetSendo Team14 min read
SPF PermError "too many DNS lookups": fix the 10-lookup limit

"SPF PermError: too many DNS lookups" means that checking your domain's SPF record needs more than 10 DNS-querying terms, the hard limit in RFC 7208, so receivers stop evaluating it and report a permanent error instead of a pass. Every message whose Return-Path uses that domain loses SPF. The fix is to count the lookups, including the nested ones hidden inside each include:, then remove dead entries, replace your own servers with ip4:/ip6:, and move third-party senders to their own subdomains.

TL;DR: Only include, a, mx, ptr, exists and redirect count toward the limit of 10, and every include counts everything nested under it; ip4, ip6 and all are free. A typical stack of Google Workspace, Mailgun, a helpdesk and a CRM can reach 17 lookups on one record. Check the Return-Path of real messages first: many includes cover senders whose envelope never uses your root domain. Give each bulk sender its own subdomain with its own SPF record and its own budget of 10; flatten to IP addresses only for vendors with stable, documented ranges.

What counts toward the SPF 10-lookup limit?

Six terms count: the include, a, mx, ptr and exists mechanisms and the redirect modifier, and once evaluation needs more than 10 of them the result must be permerror. The ip4, ip6 and all mechanisms cost nothing (RFC 7208, section 4.6.4).

The limit applies to the whole evaluation, not to the line you see in your DNS panel. Microsoft puts it plainly: an include that points to a record with three more includes costs four lookups, so "having fewer than 10 include: statements doesn't guarantee fewer than 10 DNS lookups" (Microsoft Learn: Set up SPF). Google's own help page simplifies this to "up to 10 include tags", which is only true when none of them nests further (Google Workspace: Set up SPF).

PermError

A permanent SPF error: the record is broken in a way that retrying will not fix (too many lookups, a syntax error, two SPF records on one name, an include that points to a name without an SPF record). Receivers treat it as an SPF failure, and DMARC cannot use SPF for that message.

TermCounts toward 10 (lookups)Extra rule
include:1 + everything inside itTarget without an SPF record = permerror
redirect=1 + everything inside itIgnored when the record has all
a, a:host1—
mx, mx:host1At most 10 MX hosts may be resolved, otherwise permerror
ptr1"SHOULD NOT be published" (RFC 7208, 5.5)
exists:1Often used with macros, e.g. Salesforce
ip4:, ip6:, all✓ 0—

As of November 2026, per RFC 7208.

The second limit: two void lookups

RFC 7208 also says implementations SHOULD limit "void lookups", queries that return no record or a non-existent name, to two, and exceeding that limit also produces permerror. In practice this bites when you keep includes for services you cancelled: the vendor deletes the DNS name, and your record now points at nothing. An include whose target has no SPF record is a permerror on its own (Microsoft notes the same).

One record per name

A name may publish only one TXT record that starts with v=spf1. Two records, often the result of adding a new vendor as a second record instead of merging, also return permerror. Long records can be split into several quoted strings in one TXT record; receivers join them without adding spaces (RFC 7208, 3.3).

How do you count your SPF lookups?

Query your own TXT record with dig, count the six lookup terms in it, then query every include: and redirect= target and repeat until you reach records with only ip4/ip6. The total of all levels is the number receivers see.

# 1. Your own record (exactly one line must start with v=spf1)
dig +short TXT example.com

# 2. Every include: in it, then every include: inside those
dig +short TXT mailgun.org
dig +short TXT _spf.mailgun.org

Doing this by hand gets tedious after two levels, so here is a short Bash script that walks the tree and prints it. It uses only dig, sed and tr, and runs on Linux and macOS:

#!/usr/bin/env bash
# spf-count.sh: count the SPF terms that cost a DNS lookup, following include: and redirect=
# Usage: ./spf-count.sh example.com
spf() { dig +short TXT "$1" | sed 's/" "//g; s/"//g' | grep -i '^v=spf1'; }

count() {
  local domain=$1 depth=$2 n=0 term lterm rec
  rec=$(spf "$domain")
  printf '%*s%s: %s\n' $((depth * 2)) '' "$domain" "${rec:-NO SPF RECORD}" >&2
  for term in $rec; do
    term=${term#[+~?-]}
    lterm=$(printf '%s' "$term" | tr 'A-Z' 'a-z')
    case $lterm in
      include:*)  n=$((n + 1 + $(count "${term#*:}" $((depth + 1))))) ;;
      redirect=*) n=$((n + 1 + $(count "${term#*=}" $((depth + 1))))) ;;
      a|a:*|a/*|mx|mx:*|mx/*|ptr|ptr:*|exists:*) n=$((n + 1)) ;;
    esac
  done
  echo "$n"
}

total=$(count "$1" 0)
echo "Lookups: $total of 10"
chmod +x spf-count.sh
./spf-count.sh mailgun.org

Output, shortened:

mailgun.org: v=spf1 include:_spf.mailgun.org include:_spf.eu.mailgun.org -all
  _spf.mailgun.org: v=spf1 include:_spf1.mailgun.org include:_spf2.mailgun.org ~all
    _spf1.mailgun.org: v=spf1 ip4:159.135.224.0/20 ip4:69.72.32.0/20 ...
    _spf2.mailgun.org: v=spf1 ip4:161.38.192.0/20 ip4:143.55.224.0/21 ...
  _spf.eu.mailgun.org: v=spf1 ip4:141.193.32.0/23 ip4:159.135.140.80/29 ...
Lookups: 4 of 10

The total is what an include of that name costs below it; the include itself adds one more. So include:mailgun.org costs 5 lookups in your record. The script is deliberately simple: it does not expand macros (exists:%{i}… counts as one, which is correct) and it does not count the per-host A lookups behind mx.

How much does each provider's include cost?

We ran the script against the includes that providers document for their customers. The differences are large, and a single helpdesk can use more than half of your budget.

ServiceIncludeCost in your record (lookups)
Google Workspace_spf.google.com1
Microsoft 365spf.protection.outlook.com1
Amazon SESamazonses.com1
Brevospf.brevo.com1
IONOS (EU)_spf-eu.ionos.com1
SendGridsendgrid.net2
Salesforce_spf.salesforce.com2
Zoho Mail (EU)zohomail.eu2
OVHcloudmx.ovh.com3
Mailgunmailgun.org5
Freshdeskemail.freshdesk.com7

As of November 2026: measured with the script above on 14 November 2026. Providers change their records without notice, so run it again before you rely on a number.

How do you fix "too many DNS lookups"?

Work from the cheapest change to the most invasive one: delete what is not needed, replace lookups that point at your own infrastructure with addresses, and move bulk senders to subdomains. Most domains get under 10 before flattening is needed.

Take this record, which is close to what many small companies end up with after a few years of adding tools:

; Before: 17 lookups, every message gets spf=permerror
example.com.  3600  IN  TXT  "v=spf1 a mx include:_spf.google.com include:mailgun.org include:email.freshdesk.com include:_spf.salesforce.com ~all"

That is 1 (a) + 1 (mx) + 1 (Google) + 5 (Mailgun) + 7 (Freshdesk) + 2 (Salesforce) = 17 lookups. Receivers that evaluate the whole record return permerror for every message from example.com, including ordinary staff mail.

  1. Check which senders actually use your domain in the Return-Path

    SPF checks the domain in the envelope sender (MAIL FROM, visible as Return-Path in a delivered message), not the From address. Send a test from each tool to a Gmail account and open "Show original". If the Return-Path is bounces+123@vendor-mail.com, your record is never checked for that tool and its include is dead weight. Many helpdesk, CRM and newsletter tools work this way and pass DMARC through DKIM signed with your domain instead.

  2. Remove includes for tools you no longer use

    Cancelled services are the most common cause of both lookup overruns and void lookups. If nobody can say what an include is for, look it up with dig; if its owner is a service you left, delete it.

  3. Drop ptr, and question a and mx

    ptr is slow and RFC 7208 says not to publish it. a only helps if the web server sends mail itself; mx only if your inbound mail servers also send outbound. With Google Workspace or Microsoft 365, the MX hosts are already covered by the provider's include, so mx is a wasted lookup.

  4. Replace your own servers with ip4:/ip6:

    For a server you control, such as an application server, a NetSendo MTA or an office relay, write its address directly: ip4:203.0.113.10. It costs nothing and only changes when you change the server.

  5. Move bulk and marketing senders to subdomains

    Set up Mailgun on mg.example.com, Amazon SES with a custom MAIL FROM such as bounce.example.com, and so on. Each subdomain gets its own SPF record and its own limit of 10. The next section explains why this still passes DMARC.

  6. Flatten what is left, only if you must

    If one vendor that must stay on the root domain still pushes you over, replace its include with its published address ranges and put a review date in your calendar.

After steps 1–5 the same domain looks like this:

; After: each name has its own budget of 10
example.com.     3600  IN  TXT  "v=spf1 include:_spf.google.com include:_spf.salesforce.com ~all"
mg.example.com.  3600  IN  TXT  "v=spf1 include:mailgun.org ~all"
bounce.example.com.  3600  IN  TXT  "v=spf1 include:amazonses.com ~all"
bounce.example.com.  3600  IN  MX   10 feedback-smtp.eu-west-1.amazonses.com.

The root record now costs 3 lookups, mg.example.com costs 5 and bounce.example.com costs 1, each well inside its own limit. Freshdesk is gone from the root because, in this example, its Return-Path was the vendor's domain. The MX record on the SES subdomain is required by Amazon SES for a custom MAIL FROM domain, and the SPF record there is the one SES documents (Amazon SES: custom MAIL FROM domain). Use the region of your SES account in the MX host.

⚠️ Warning: Do not remove an include before you have confirmed, on a real message, where that tool's Return-Path points. Removing a sender whose envelope does use your root domain turns its SPF result into fail; it will only pass DMARC if it also signs with DKIM aligned to your domain.

Is SPF flattening a good idea?

Flattening replaces an include with the IP ranges it currently resolves to, so the sender costs no lookups. It works, but you take over a job the vendor was doing for you: noticing when their addresses change.

; Flattened by hand: 0 lookups for this sender, but the list goes stale when the vendor adds a range
example.com.  3600  IN  TXT  "v=spf1 include:_spf.google.com ip4:198.51.100.0/24 ip4:203.0.113.0/25 ~all"

Microsoft's guidance is a good rule of thumb. Flatten only vendors with stable, documented ranges, never Microsoft 365 itself, whose sending addresses change frequently, and if you do flatten, document what you replaced, watch vendor documentation, review the entries at least quarterly and test after each change (Microsoft Learn).

Static vs automated flattening

Static flattening is the example above: you copy the addresses once. It is free and transparent, and it breaks silently the day the vendor adds a range, because mail from the new range fails SPF while everything looks fine in your DNS panel. Automated flattening services re-resolve the includes on a schedule and publish the result, usually through an include or a CNAME to their own zone. That keeps the list current but adds a paid dependency in your authentication path; if the service fails, so does your SPF.

✅ Advantages

  • Removes the lookups of the flattened vendor completely
  • Faster evaluation, fewer DNS timeouts
  • No change to how the vendor sends

❌ Considerations

  • Stale addresses lead to SPF fails that nobody notices
  • Long address lists push the record past the 450-octet guideline of RFC 7208, 3.4
  • Automated services add a third party to your mail authentication
  • Does not fix the root cause: too many senders on one name

Why does a sending subdomain avoid the problem?

Because SPF is evaluated for the exact domain in the envelope sender, and every name has its own record and its own budget of 10 lookups. Microsoft says it directly: "Each subdomain has its own 10 lookup budget", and it recommends subdomains for email services you don't control (Microsoft Learn).

DMARC is what makes this work for the visible From address. DMARC uses only the SPF result for the MAIL FROM identity, and that identity must be aligned with the From domain (RFC 9989, 4.4.2). Alignment is relaxed by default (aspf=r), so bounce.example.com and news@example.com share the organisational domain example.com and count as aligned. You can keep sending from news@example.com while SPF is checked on the subdomain.

It also matches what Gmail asks of every sender: SPF and DKIM set up for your domain, aligned at the organisational level (Google: Email sender guidelines). A separate subdomain for marketing has a second benefit: bounces and complaints from campaigns land on a name you can watch on its own. For a full walk-through of SPF, DKIM, DMARC and warm-up for your own servers, see our email deliverability guide for self-hosted senders.

💡 Tip: If your DMARC record sets aspf=s (strict), a subdomain envelope is not aligned and SPF stops counting for DMARC. Check your record with dig +short TXT _dmarc.example.com before you move senders.

Should the record end in ~all or -all?

Once DMARC is in place, either works, because DMARC treats both ~all (softfail) and -all (fail) as SPF failures. Google recommends ~all (Google Workspace); Microsoft recommends -all for Microsoft 365 domains that also use DKIM and DMARC (Microsoft Learn).

The difference is what happens before DMARC runs. RFC 9989 warns that some receivers check SPF early in the SMTP session, so -all can get a message rejected even though it would have passed DMARC through an aligned DKIM signature, and such rejections never show up in your aggregate DMARC reports (RFC 9989, 7.1). This matters for forwarded mail and mailing lists. A practical rule:

  • While you are still cleaning up includes and moving senders, use ~all.
  • For a subdomain used by one sender only, such as mg.example.com, -all is low-risk: nothing else should send from it.
  • For the root domain with staff mail, ~all plus DMARC at p=quarantine or p=reject gives you enforcement without early SPF rejections.

Neither choice fixes a permerror: with too many lookups, the all at the end is never reached.

How do you verify the fix?

Check the published record from outside your network, re-run the count, then send a real message and read the Authentication-Results header. Expect spf=pass for the envelope domain within the TTL of the old record.

# The record the world sees (ask a public resolver, not your cache)
dig +short TXT example.com @1.1.1.1

# Exactly one SPF record per name
dig +short TXT example.com @1.1.1.1 | grep -c 'v=spf1'

# The count after the change
./spf-count.sh example.com 2>/dev/null

Then send a message from each tool to a Gmail or Outlook.com mailbox and look at the headers. A broken record looks like this, with DKIM still saving the DMARC result:

Authentication-Results: mx.receiver.example;
       spf=permerror smtp.mailfrom=news@example.com;
       dkim=pass header.d=example.com;
       dmarc=pass header.from=example.com

After the fix you want spf=pass and an smtp.mailfrom on the domain you planned. If you get the result dmarc=fail with SPF and DKIM both failing, see our guide to the Gmail 550-5.7.26 unauthenticated error.

📋 Checklist

  • One v=spf1 record per name
  • 10 lookups or fewer, counted with nested includes
  • No includes for cancelled services, no ptr
  • Bulk senders on their own subdomains, each with its own record
  • TTL of 3600 seconds, as Microsoft recommends, so changes spread within an hour
  • DMARC aggregate reports show SPF passing for every source you know

Keep watching your DMARC aggregate (rua) reports for two weeks after the change: a source that suddenly shows SPF fail is usually a sender you did not know about, sending from the root domain.

SPF lookups when you send with NetSendo

NetSendo publishes no SPF include of its own, so it never adds a nested chain to your record. What your SPF needs depends on how a mailbox sends:

  • NetSendo MTA (built into every installation since 2.11.0): the sending domain's record must list the server's public address, or the mailbox's dedicated IP, as ip4:/ip6:. That costs no lookup. The panel shows the exact entry to add.
  • SMTP relays: NetSendo recognises Amazon SES, SendGrid, Google Workspace, Microsoft 365 and Mailgun by their SMTP host. For Amazon SES and SendGrid it adds no include, because those relays send with their own envelope domain (or the custom MAIL FROM you set up with them).
  • Return-Path: when bounce handling is on for a mailbox, NetSendo sets the Return-Path to that mailbox's bounce address, so a bounce address on a subdomain moves the SPF check there. Setup is in the configuration docs.

On GOLD, the Deliverability module checks SPF, DKIM and DMARC for each domain, and its SPF generator keeps your existing record and only adds the includes your NetSendo mailboxes need. It shows a lookup count and flags records above 8. That count covers the terms in the record itself and does not expand nested includes, so for a record full of third-party includes, run the script from this article as well. To get started, follow the quickstart.

📌 Key takeaways

  • The limit is 10 lookup terms across the whole tree, not 10 includes.
  • Check Return-Paths before editing: many includes protect nothing.
  • Subdomains give every bulk sender its own budget and still align for DMARC.
  • Flatten only stable, documented ranges, and review them every quarter.

🎯 Expert tips

1
Keep a sender inventory next to the record

Write down, for every term in your SPF record, which tool it is for, who owns it and when it was added. The next permerror usually comes from an include nobody can explain.

2
Leave headroom

Aim for 7 or 8 lookups, not 10. Vendors add nested includes without notice, and a record at exactly 10 can break overnight.

3
Make onboarding a new tool a DNS review

When marketing buys a new tool, decide on its subdomain before anyone pastes an include into the root record.

4
Lower the TTL before a big change

Drop the TTL to 300 seconds a day before you restructure the record, and raise it back to 3600 once the new record is confirmed.

Frequently asked questions

What does SPF PermError too many DNS lookups mean?

Evaluating your SPF record needed more than the 10 DNS-querying terms allowed by RFC 7208, so the receiver stopped and returned a permanent error. The message is treated as failing SPF, and DMARC cannot use SPF for it.

Which SPF mechanisms count toward the 10 DNS lookup limit?

include, a, mx, ptr and exists, plus the redirect modifier. Every include also counts all lookups nested inside it; ip4, ip6 and all do not count.

Does each subdomain have its own SPF lookup limit?

Yes. SPF is evaluated for the exact domain in the envelope sender, and every name has its own record and its own limit of 10. Microsoft recommends moving third-party senders to subdomains for this reason.

Is SPF flattening safe?

It works, but the copied IP addresses go stale when the vendor changes its ranges, and mail from new ranges then fails SPF. Flatten only vendors with stable, documented ranges and review the entries at least quarterly.

Does a permerror with too many lookups fail DMARC?

Only if DKIM fails too. DMARC passes when either SPF or DKIM passes and is aligned with the From domain, so an aligned DKIM signature still lets the message pass.

How many include statements can an SPF record have?

There is no fixed number of includes; the limit is 10 lookup terms across the whole evaluation. A single include that nests six more costs seven lookups.

Sources

Send from your own server with a short SPF record

NetSendo's built-in MTA needs one ip4: entry in your SPF record and no include chain.

#spf#dns#email authentication#dmarc#deliverability
Share