# SPF Too Many DNS Lookups: Fix PermError and the 10-Lookup Limit

Source: https://srvscripts.com/guides/spf-too-many-dns-lookups/
Updated: 2026-10-06
Publisher: srvScripts (https://srvscripts.com/)

**Short answer:** An SPF record may trigger at most 10 DNS-querying terms (`include`, `a`, `mx`, `ptr`, `exists` and `redirect`, counted through every nested include). Go over that and receivers return `permerror`, which counts as an SPF failure for DMARC and for the Gmail, Yahoo and Outlook.com sender rules. Fix it by removing senders you no longer use, swapping `a` and `mx` for `ip4`/`ip6`, and giving each bulk sending service its own subdomain. Flatten only if you can automate the updates.

We ran the lookup counts below on our lab server (AlmaLinux 9.8, cPanel & WHM 11.138) on 6 October 2026. The SPF rules are checked against RFC 7208 (linked below). Third-party include costs change when providers edit their records, so count your own before you edit.

## What “too many DNS lookups” means

RFC 7208 section 4.6.4 limits how much DNS work one SPF check can cause. Receivers count every term that needs a DNS query while they evaluate your record, including terms inside the records you include. Once the count goes past 10, the result is `permerror`. Checkers show it in different ways: `PermError: too many DNS lookups`, `SPF permanent error: Too many DNS lookups`, or `spf=permerror` in the `Authentication-Results` header.

| Term | Counts toward the 10? | Notes |
| --- | --- | --- |
| include: | Yes, 1 each | Plus everything inside the included record |
| a, a:host | Yes, 1 each |  |
| mx, mx:host | Yes, 1 each | Each mx may also resolve at most 10 MX hosts to addresses, or it returns permerror |
| ptr | Yes | RFC 7208 says it should not be used at all |
| exists: | Yes, 1 each | Common with macro-based records |
| redirect= | Yes | Plus everything in the target record |
| ip4:, ip6:, all | No | No DNS query needed |
| exp= | No | Looked up only after a fail, for the explanation text |

There is a second limit that people miss: **void lookups**. A void lookup is a query that returns no answer (NOERROR with zero records, or NXDOMAIN). RFC 7208 says receivers should allow no more than two, and going past that also produces `permerror`. An `include:` that points at a domain with no SPF record at all is worse: RFC 7208 maps it straight to `permerror`, even if you are well under 10 lookups.

A permerror does not mean “neutral”. DMARC treats it as no SPF pass, and Gmail, Yahoo and Outlook.com require SPF to pass for bulk senders. If DKIM is aligned, DMARC can still pass, but you lose your backup path.

## Count your lookups

The fastest check is our [SPF Record Checker](/tools/spf-record-checker/), which walks the include tree. From a shell you can do the same walk with `dig`. Start with your own record:

```
dig +short TXT example.com | grep -i "v=spf1"
```

Then query every `include:` and `redirect=` target, and every include inside those, and add up the DNS-querying terms. We use this small read-only helper on our lab server to do the walk. It is a rough counter. It does not expand macros or count the extra address lookups an `mx` term makes, so treat the result as a minimum.

```
#!/usr/bin/env bash
# spf-count.sh - rough count of DNS-querying SPF terms (include, a, mx, ptr, exists, redirect)
# Usage: spf-count.sh example.com
set -uo pipefail
total=0
walk() {
  local dom=$1 depth=$2 rec term
  rec=$(dig +short TXT "$dom" | sed -e 's/" "//g' -e 's/"//g' | grep -i '^v=spf1' | head -n1)
  # long records come back as several quoted strings: join them with no space (RFC 7208 3.3)
  [ -z "$rec" ] && { printf '%*s%s: no SPF record\n' "$depth" '' "$dom"; return; }
  printf '%*s%s\n' "$depth" '' "$dom: $rec"
  for term in $rec; do
    term=${term#[+~?-]}
    case "${term,,}" in
      include:*)  total=$((total+1)); walk "${term#*:}" $((depth+2)) ;;
      redirect=*) total=$((total+1)); walk "${term#*=}" $((depth+2)) ;;
      a|a:*|a/*|mx|mx:*|mx/*|ptr|ptr:*|exists:*) total=$((total+1)) ;;
    esac
  done
}
walk "$1" 0
echo "DNS-querying terms: $total (limit 10)"
```

Output from our lab run on 6 October 2026, with the long IP lists replaced by `[ip ranges]`:

```
$ bash spf-count.sh mailgun.org
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 [ip ranges] ~all
    _spf2.mailgun.org: v=spf1 [ip ranges] ~all
  _spf.eu.mailgun.org: v=spf1 [ip ranges] ~all
DNS-querying terms: 4 (limit 10)
```

So `include:mailgun.org` in your record costs 5 lookups: 1 for the include itself and 4 inside it. That is half your budget for one service. Here is what some common includes cost on the day we checked:

| Include in your record | Lookups it costs (6 Oct 2026) |
| --- | --- |
| include:spf.protection.outlook.com (Microsoft 365) | 1 |
| include:_spf.google.com (Google Workspace) | 1 |
| include:sendgrid.net | 2 |
| include:mailgun.org | 5 |
| include:_spf.salesforce.com | 2 (uses an exists macro) |
| include:amazonses.com | 1 |
| +a +mx (cPanel default record) | 2 |

A typical small business record that adds Microsoft 365, Google Workspace for one department, Mailgun for the shop and the hosting server (`a` + `mx`) is already at 9. One more marketing tool tips it over.

## Fix 1: remove senders you no longer use

Most records that break the limit carry includes for services nobody uses any more: an old newsletter tool, a CRM trial, a previous helpdesk. Before you remove one, check that it really stopped sending:

- Look at your DMARC aggregate reports for the last 30 days. Our [DMARC Report Analyzer](/tools/dmarc-report-analyzer/) groups the source IPs by provider. A service with zero volume can go.

- Ask the people who own each service (marketing, support, finance) before you delete its include.

- Remove `ptr` terms. RFC 7208 says the mechanism is slow, unreliable and should not be used.

- Remove duplicates, for example an `include:` that a parent include already pulls in.

## Fix 2: replace a and mx with ip4 and ip6

cPanel publishes a default SPF record of the form `v=spf1 +a +mx +ip4:203.0.113.10 ~all`. On our lab the zone file had exactly that pattern. The `a` and `mx` terms cost two lookups and usually point at the same server the `ip4` already covers. If your website and MX are on the hosting server, list its addresses directly:

```
v=spf1 ip4:203.0.113.10 ip6:2001:db8::10 include:spf.protection.outlook.com ~all
```

Only do this if you control those addresses. If the server IP changes during a migration, the SPF record must change with it, so add it to your migration checklist. If you later click **Repair** in the cPanel Email Deliverability page, read the suggested record first so the `a` and `mx` terms do not come back.

## Fix 3: give each bulk sender its own subdomain

SPF is checked against the envelope sender (the `MAIL FROM` or Return-Path domain), not the visible From address. DMARC with relaxed alignment (the default) passes when that domain is the same organizational domain as the From address, so `bounce.example.com` aligns with `From: news@example.com`.

Most email service providers let you set a custom Return-Path or “custom MAIL FROM” domain. When you do that, the provider’s include goes into the SPF record of that subdomain, not the root:

```
; root domain: only what sends with an @example.com envelope
example.com.          TXT "v=spf1 ip4:203.0.113.10 include:spf.protection.outlook.com -all"
; newsletter provider uses its own envelope subdomain
bounce.example.com.   TXT "v=spf1 include:mailgun.org -all"
```

Each subdomain gets its own 10-lookup budget. It also keeps reputation separate: a complaint spike on the newsletter does not touch the domain your invoices use. Follow the provider’s own steps for the subdomain record, since some of them use a CNAME instead of a TXT record.

## Fix 4: flattening, and why it is a last resort

Flattening means replacing `include:` terms with the `ip4`/`ip6` ranges they currently resolve to. It brings the count to zero, but it has real costs:

- **Providers change their ranges without telling you.** When Microsoft, Google or your ESP adds a range, mail from the new range fails SPF until you update. Flattening only works with a job that re-resolves the includes and rewrites the record automatically.

- **Size.** RFC 7208 section 3.4 asks you to keep the DNS answer under 512 octets so it does not need TCP. Long flattened records go past that quickly.

- **255-character strings.** A TXT string cannot exceed 255 characters, so long records must be split into several quoted strings. Receivers join the strings with no space added, so if the space between two terms falls at the split point and is lost, the terms run together.

- **You lose the provider’s own changes** such as a nested include for a new region.

If you do flatten, flatten only the includes with stable, published ranges and leave the rest as includes. Better still, move the heavy sender to a subdomain (Fix 3) and keep the root record short.

## Check that it worked

- Query the published record: `dig +short TXT example.com`. Make sure there is exactly one string group starting with `v=spf1`.

- Run it through the [SPF Record Checker](/tools/spf-record-checker/) and confirm the count is 10 or less, with no void lookups.

- Send a test message to a Gmail or Outlook.com mailbox, open the original message, and paste the headers into our [Email Header Analyzer](/tools/email-header-analyzer/). You want `spf=pass` in `Authentication-Results`, not `spf=permerror`.

- Watch the next week of DMARC aggregate reports for SPF results from each sending source.

## Common problems

- **Two SPF records.** If more than one TXT record at the same name starts with `v=spf1`, RFC 7208 says the result is `permerror`. Merge them into one. This often happens when a panel adds its own record next to one you created.

- **An include of a domain with no SPF record.** A typo such as `include:_spf.gogle.com`, or a provider that retired its SPF host, gives permerror. Check each include with `dig`.

- **A broken split.** A long record entered as two strings where the space between terms was lost: `"...include:a.example" "include:b.example..."` is read as `include:a.exampleinclude:b.example`. Put the space inside one of the strings.

- **Using the old SPF record type.** Publish SPF as a TXT record. The separate SPF (type 99) record type is not used.

- **Forgetting the HELO name.** Receivers may also check SPF for the HELO/EHLO hostname. Give the server hostname its own simple record such as `v=spf1 a -all`.

**Official documentation:** [RFC 7208: Sender Policy Framework (SPF)](https://datatracker.ietf.org/doc/html/rfc7208) · [Gmail email sender guidelines](https://support.google.com/a/answer/81126) · [Outlook.com high-volume sender requirements](https://techcommunity.microsoft.com/blog/microsoftdefenderforoffice365blog/strengthening-email-ecosystem-outlook%E2%80%99s-new-requirements-for-high%E2%80%90volume-senders/4399730)

**Related:** [SPF Record Checker](/tools/spf-record-checker/) · [SPF Record Generator](/tools/spf-record-generator/) · [SPF, DKIM and DMARC in cPanel DNS: Setup and Checks](/guides/spf-dkim-dmarc-cpanel-dns/) · [Microsoft 365 SPF DKIM DMARC: Secure Exchange Online Setup](/guides/microsoft-365-spf-dkim-dmarc-exchange-online/) · [MailBaby SPF Record: spf-c Include vs _mailbaby TXT Guide](/guides/mailbaby-spf-record-domain-verification/)

**See also:** [SPF Lookup Counter: Check the 10 DNS Lookup Limit](/tools/spf-lookup-counter/) · [DNS TXT Record Splitter: Split Long Values Into 255-Byte Strings](/tools/dns-txt-splitter/)

## Frequently asked questions

### What is the SPF 10 DNS lookup limit?

RFC 7208 limits an SPF check to 10 terms that cause DNS queries: include, a, mx, ptr, exists and redirect, counted across all nested includes. ip4, ip6 and all do not count. Going over returns permerror.

### Does ip4 or ip6 count as a DNS lookup?

No. ip4, ip6 and all need no DNS query, so they never count toward the limit. That is why replacing a and mx with your server addresses saves lookups.

### What is an SPF void lookup?

A DNS query made during the SPF check that returns no records or NXDOMAIN. RFC 7208 says receivers should allow at most two before returning permerror.

### Is SPF flattening safe?

Only with automation. Flattening copies provider IP ranges into your record, and when the provider changes its ranges your mail starts failing SPF until the record is updated.

### Will DMARC still pass if SPF has a permerror?

It can, if the message also has a DKIM signature that passes and aligns with the From domain. DMARC needs only one aligned pass, but you lose SPF as a backup.

### Do subdomains share the root domain’s 10 lookups?

No. Each domain name has its own SPF record and its own limit. A sender that uses bounce.example.com as its envelope domain is checked against that record only.
