Remediation Guides

How to Fix SPF PermError: Too Many DNS Lookups (10-Lookup Limit)

26 September 2026 8 min read

SPF PermError “too many DNS lookups” means that evaluating your domain’s SPF record needs more than 10 DNS-querying terms (include, a, mx, ptr, exists and redirect, counted through every nested include), so receivers return permerror instead of pass. Fix it by removing unused includes, replacing a and mx with ip4/ip6, flattening stable vendors or moving senders to subdomains.

What the scanner is actually detecting

The finding comes from the ProjectDiscovery Nuclei template spf-limit-lookup, titled SPF record DNS lookup limit, severity info. The template queries the TXT records of the exact name you scanned, extracts the string starting with v=spf1, runs the case-insensitive regex (include|all):?[^ ]*? over it, and reports a match when the number of extracted values is greater than 10.

That is a text heuristic, not an SPF evaluation. It reads only the top-level record, never follows nested includes, ignores a, mx, ptr, exists and redirect, and counts all, which costs no lookup. Nuclei also stores regex extractor results as a set of unique strings, so the number compared against 10 is not a plain term count. Treat a hit as a reason to investigate, a clean result as proof of nothing, and count the real way.

The real rule is RFC 7208, section 4.6.4. The limit is global across the whole evaluation, including every nested include and redirect:

Term Counts toward the 10? Notes
include: Yes Plus every lookup inside the included record
redirect= Yes Plus every lookup in the target record
a, mx, exists Yes An mx needing more than 10 address lookups is itself a permerror
ptr Yes RFC 7208 says it SHOULD NOT be published
ip4:, ip6:, all, exp= No No DNS query during evaluation

A second limit hides here: implementations SHOULD allow no more than two “void lookups” (answers with no records, or NXDOMAIN). Exceeding it is also permerror.

Real-world risk

Nothing on your servers is exposed. The impact is on mail authentication:

  • Legitimate mail fails SPF. A receiver that rejects on permerror should use SMTP code 550 with status 5.5.2 (RFC 7208). Microsoft notes the bounce may say the message required too many lookups.
  • DMARC loses SPF. A permerror is not a pass, so DMARC can only pass on an aligned DKIM signature. Senders that do not sign with DKIM can fail DMARC, and with a quarantine or reject policy that affects delivery.
  • It looks intermittent. Receivers evaluate left to right and stop at the first match, so senders listed early can still pass while senders further right hit the limit.
  • Spoofing protection weakens. Forged mail from an unlisted IP walks the whole record, so it gets permerror instead of fail.

This is a deliverability and hygiene problem, not an emergency, but it tends to worsen silently as vendors add nested includes.

How to confirm it

Start with the published record and each include it references:

dig +short TXT example.com | grep -i 'v=spf1'
dig +short TXT _spf.vendor.example

Walking a deep tree by hand is error-prone. This bash script follows include and redirect recursively and counts the terms from the table (tested with bash 5 and dig; macros are not expanded):

#!/usr/bin/env bash
# spf-count.sh: count SPF terms that cost a DNS lookup (RFC 7208, 4.6.4)
# usage: ./spf-count.sh example.com
set -f; shopt -s nocasematch

spf_count() {
  local domain=$1 indent=$2 rec term total=0 sub
  rec=$(dig +short TXT "$domain" | grep -i '^"v=spf1' | sed 's/" "//g; s/"//g')
  if [ -z "$rec" ]; then
    echo "${indent}${domain}: NO SPF RECORD" >&2; echo 0; return
  fi
  [ "$(printf '%sn' "$rec" | wc -l)" -gt 1 ] && echo "${indent}${domain}: MULTIPLE SPF RECORDS" >&2
  echo "${indent}${domain}: ${rec}" >&2
  for term in $rec; do
    term=${term#[-+~?]}
    case "$term" in
      include:*)  sub=$(spf_count "${term#*:}" "$indent  "); total=$((total + 1 + sub)) ;;
      redirect=*) sub=$(spf_count "${term#*=}" "$indent  "); total=$((total + 1 + sub)) ;;
      a|a:*|a/*|mx|mx:*|mx/*|ptr|ptr:*|exists:*) total=$((total + 1)) ;;
    esac
  done
  echo "$total"
}

n=$(spf_count "$1" "")
echo "DNS-querying terms: $n (limit 10)"

The same logic in PowerShell on Windows:

function Get-SpfLookupCount([string]$Domain, [string]$Indent = '') {
  $recs = @(Resolve-DnsName -Name $Domain -Type TXT -DnsOnly -ErrorAction SilentlyContinue |
    Where-Object Type -eq 'TXT' | ForEach-Object { $_.Strings -join '' } |
    Where-Object { $_ -match '^v=spf1( |$)' })
  if ($recs.Count -eq 0) { Write-Host "$Indent$Domain : NO SPF RECORD"; return 0 }
  if ($recs.Count -gt 1) { Write-Host "$Indent$Domain : MULTIPLE SPF RECORDS" }
  Write-Host "$Indent$Domain : $($recs[0])"
  $total = 0
  foreach ($t in ($recs[0] -split 's+')) {
    $t = $t.TrimStart('+', '-', '~', '?')
    if ($t -match '^(include:|redirect=)(.+)$') { $total += 1 + (Get-SpfLookupCount $Matches[2] "$Indent  ") }
    elseif ($t -match '^(a|mx|ptr)([:/].*)?$' -or $t -match '^exists:') { $total += 1 }
  }
  return $total
}
Get-SpfLookupCount 'example.com'

Anything above 10 is the real problem. A “NO SPF RECORD” line under an include is a separate permerror: RFC 7208 treats an include that finds no record that way. On the receiving side, the Authentication-Results header of a test message shows spf=permerror, and DMARC aggregate reports list it per source IP.

How to fix it

1. Remove includes nobody uses

Old CRM, ticketing and newsletter services are the usual culprits. Check DMARC aggregate reports over a full business cycle: an include whose ranges never appear as a source can usually go. The same include reached twice, directly or through another record, also wastes lookups. Stale includes are a security issue too, as covered in our guide to auditing forgotten domains and stale DNS records.

2. Replace a, mx and ptr with ip4/ip6

RFC 7208 section 10.1.1 calls explicit addresses the best record. The mx mechanism authorizes the hosts that receive your mail, which are not always the hosts that send it.

# before: 2 lookups (a, mx)
v=spf1 a mx include:spf.protection.outlook.com -all
# after: 0 lookups for your own relays
v=spf1 ip4:203.0.113.10 ip4:203.0.113.11 include:spf.protection.outlook.com -all

3. Move third-party senders to subdomains

Microsoft recommends subdomains for services you do not control, and notes that each subdomain gets its own 10-lookup budget. Configure the vendor to use the subdomain as its envelope sender (often called a custom return-path or bounce domain), then publish a record there. With DMARC’s default relaxed SPF alignment, news.example.com still aligns with a From address at example.com.

example.com.       IN TXT "v=spf1 ip4:203.0.113.10 include:spf.protection.outlook.com -all"
news.example.com.  IN TXT "v=spf1 include:servers.bulkvendor.example -all"

Note that moving includes into a record you host yourself (for example include:_spf.example.com) saves nothing. The limit is global, so nested lookups still count.

4. Flatten only stable vendors

Flattening replaces an include with the ip4/ip6 ranges it resolves to. Read them with dig +short TXT on the vendor’s include, or better, from the vendor’s published IP documentation. Microsoft says not to flatten include:spf.protection.outlook.com or any service whose IPs change often, and to document flattened entries, watch vendor changes and review them at least quarterly.

Flattened records get long. RFC 7208 section 3.4 advises keeping the DNS answer within 512 octets. A TXT string holds at most 255 characters, and multiple strings are joined without spaces, so keep the space inside the string:

@  3600  IN  TXT  ( "v=spf1 ip4:203.0.113.10 ip4:198.51.100.0/24 "
                   "include:spf.protection.outlook.com -all" )
named-checkzone example.com /path/to/db.example.com
rndc reload example.com

On hosted DNS, edit the existing TXT value rather than adding a second SPF record, which would cause a different permerror.

How to verify the fix and rescan

  1. Run the counter again. Aim for a total at or below 10 with a lookup or two of headroom, one SPF record per name and no “NO SPF RECORD” lines.
  2. Resolvers may serve the old record until its TTL expires. Query an authoritative server directly with dig @ns1.your-dns-host.example TXT example.com +norecurse if you need to confirm immediately.
  3. Send a test message from every sender, including the ones listed last, and check for spf=pass in Authentication-Results.
  4. Watch DMARC aggregate reports for permerror disappearing over the next few days.
  5. Rerun the scan: nuclei -u example.com -id spf-limit-lookup (add -debug to see the TXT answer). Given the template’s heuristic, keep the counter output as closure evidence.

What can break and how to roll back

  • Removing an include still in use. That sender now fails SPF, and with -all its mail may be rejected.
  • Stale flattened IPs. When the vendor changes ranges, its mail fails SPF without any change on your side.
  • Half-finished subdomain moves. If the vendor still uses your apex as envelope sender, removing its include from the apex breaks it.
  • Bad string splits. A missing space between strings merges two terms into one invalid term.

Save the current TXT value before every change. Rolling back means publishing it again (on BIND, restore the zone file, increment the serial and run rndc reload). Caches hold the newer answer for up to the TTL.

Common false positive reasons

  • The template counts text, not lookups. Its regex matches all and include anywhere in the record, including inside domain names, and none of that equals the RFC count.
  • Exactly 10 is allowed. RFC 7208 returns permerror only when the limit is exceeded.
  • Stale result. The record changed after the scan, or a vendor trimmed its own nested includes.
  • Wrong name scanned. The template checks the exact FQDN, which may not be a domain you send mail from.

The reverse is more common: a record with a handful of includes that expand into many nested lookups exceeds the real limit and never triggers the template.

FAQ

Do ip4 and ip6 entries count toward the 10-lookup limit?

No. RFC 7208 exempts ip4, ip6, all and exp. Only include, a, mx, ptr, exists and redirect count, including nested ones.

Is exactly 10 DNS lookups OK?

Yes. Permerror starts at 11. Leave headroom anyway, because vendors can add nested includes to their records at any time.

Is SPF flattening safe?

For vendors with stable, documented ranges, yes, if you maintain it. Microsoft advises against flattening Microsoft 365 or services with frequently changing IPs.

Will moving my includes into a separate TXT record fix it?

No. An include of your own sub-record costs a lookup itself, and everything inside it still counts toward the same global limit.

Tracking this finding across many domains

SPF lookup findings tend to recur across many domains and subdomains, often from more than one tool. SITEY, a self-hosted vulnerability management platform, imports findings from 16 scanners and merges duplicates per scanner, not across scanners, so the same domain flagged by two tools remains two items to close. Its AI triage can suggest likely false positives with evidence, and a human makes the final call.

Sources

SITEY closes the loop, not just the report.Discover, validate, fix and verify in your own infrastructure.

See pricing