# SPF's 10 DNS lookup limit: why SPF breaks and how to fix it

> An SPF record may trigger at most 10 DNS lookups during one check. Includes count recursively, so a record that worked last year can go over when a provider edits theirs, and the result is permerror. Remove unused includes, move heavy senders to a subdomain, and rely on DKIM for the rest.

By the DNS Autopilot team. Published 2026-10-18.

SPF limits every check to 10 DNS lookups. If a receiver needs an 11th lookup to evaluate your record, it stops and returns `permerror`, a permanent error. SPF does not pass, no matter how correct your IP addresses are. The limit is in [section 4.6.4 of RFC 7208](https://www.rfc-editor.org/rfc/rfc7208#section-4.6.4).

You usually find out through a symptom, not a warning: a DMARC report with SPF failures, a "too many DNS lookups" error in a checker, or legitimate mail landing in spam. This post explains what counts toward the 10, how to count your own record, and how to fix it, starting with the safest change.

## Why a permerror hurts

SPF checks the envelope sender domain (the MAIL FROM address, which shows up later as `Return-Path`) and the HELO name. It does not check the visible From address. A permerror means the receiver cannot tell whether the sending server is authorized.

DMARC ([RFC 7489](https://www.rfc-editor.org/rfc/rfc7489)) passes if either aligned SPF or aligned DKIM passes. Aligned SPF means SPF passes and the Return-Path domain matches the From domain. Relaxed alignment, the default, accepts the same organizational domain. Strict alignment needs an exact match.

When SPF returns permerror, DMARC has to rely on aligned DKIM. If DKIM also fails or is not aligned, DMARC fails. With `p=quarantine` or `p=reject`, that mail goes to spam or is rejected. Some receivers also treat permerror harshly even without DMARC. If you are still on `p=none`, nothing is blocked yet, but you cannot safely move to enforcement until this is fixed. See [moving DMARC from p=none to p=reject](https://dnsautopilot.com/blog/dmarc-p-none-to-p-reject).

## What counts toward the limit

The terms that cause DNS lookups are the `include`, `a`, `mx`, `ptr`, and `exists` mechanisms and the `redirect` modifier. Their total during one evaluation must not exceed 10.

| Term | Counts toward 10? | Notes |
| --- | --- | --- |
| `include:` | Yes, 1 each | Plus everything inside the included record, recursively |
| `a` | Yes, 1 | The address lookup for the host |
| `mx` | Yes, 1 | The MX query counts. The address lookups for the MX hosts do not, but one `mx` may resolve at most 10 MX names (more is a permerror) |
| `ptr` | Yes, 1 | Deprecated: RFC 7208 says it SHOULD NOT be used |
| `exists:` | Yes, 1 | Often used for custom macros |
| `redirect=` | Yes, 1 | Plus everything in the target record |
| `ip4:` / `ip6:` | No | Literal addresses and ranges need no lookup |
| `all` | No | The ending needs no lookup |
| `exp=` | No | Not counted |

A second limit is easy to miss. Void lookups, meaning queries that return NXDOMAIN or an empty answer, should be limited to 2, the default RFC 7208 recommends, and most receivers return permerror on the third. This usually happens when you include a provider record that has since been deleted.

## How nesting adds up

Includes count recursively. An include whose record contains 3 more includes costs 4 in total: 1 for the include itself and 3 for the ones inside it. Those inner records can have includes of their own.

This is why SPF breaks without anyone touching DNS. You include a provider's record, and the provider later adds a nested include for a new data center or a new product. Your record is unchanged, but your count went up. A record that was at 9 last year is at 11 today.

Some practical consequences:

- You cannot judge a record by its length. A short record with four includes can cost more than a long one made of `ip4` ranges.
- You do not control the count of your includes. Leave headroom of 2 or 3 lookups when you can.
- Each name has its own budget. The 10 applies to the record being evaluated, starting from the domain in the Return-Path. A subdomain with its own SPF record gets a fresh 10.

## Count your own lookups

Start with your domain's record. Use `dig`:

```bash
dig +short TXT example.com
```

Or DNS over HTTPS, if `dig` is not available:

```bash
curl -s 'https://dns.google/resolve?name=example.com&type=TXT' | jq -r '.Answer[].data'
```

Find the line that starts with `v=spf1`. Then run the same query for every `include:` and `redirect=` target, and repeat for any includes you find inside those. Here is a made-up record:

```dns
example.com.  TXT  "v=spf1 include:_spf.crm.example include:mail.newsletter.example include:_spf.helpdesk.example a mx -all"
```

Query each include and follow it down:

```dns
_spf.crm.example.            TXT  "v=spf1 include:_netblocks.crm.example include:_netblocks2.crm.example -all"
mail.newsletter.example.     TXT  "v=spf1 include:spf.relay.newsletter.example ~all"
_spf.helpdesk.example.       TXT  "v=spf1 ip4:192.0.2.0/24 ip6:2001:db8::/32 -all"
```

Now count, adding one for every term that triggers a lookup:

```text
include:_spf.crm.example               1
  include:_netblocks.crm.example       1
  include:_netblocks2.crm.example      1
include:mail.newsletter.example        1
  include:spf.relay.newsletter.example 1
include:_spf.helpdesk.example          1   (ip4 and ip6 inside are free)
a                                      1
mx                                     1
-all                                   0
---------------------------------------------
Total                                  8 of 10
```

That works, with 2 to spare. Now suppose someone adds `include:_spf.billing.example`, and that record has 3 nested includes. That is 4 more lookups, for a total of 12. Every message evaluated against this record gets a permerror.

Counting by hand is tedious and easy to get wrong, especially when includes are four levels deep. The [free check on DNS Autopilot](https://dnsautopilot.com/check) does it for you: it reads the SPF record, follows every include, counts the lookups against the limit of 10, and shows how the record ends.

## Fixes, from safest to riskiest

Work down this list. The first four do not change who is authorized to send. The fifth does, and it needs care.

### 1. Remove includes for services you no longer use

Old CRMs, retired newsletter tools, a trial of a helpdesk: each one may still be in your record. Check the vendor's dashboard or ask the team. If nothing sends through a service, delete its include. Also delete includes that point to provider records that no longer exist, since they add void lookups.

### 2. Find out which services actually use your domain in the Return-Path

Many sending services use their own bounce domain as the Return-Path by default. SPF is then checked against the provider's domain, not yours. Your SPF record plays no part for that mail, and your include for the service is dead weight.

Those services can still pass DMARC through DKIM. In each service, set up DKIM signing with your own domain. This is often called domain authentication or sender authentication. Once the message is signed with `d=example.com`, DKIM aligns with your From domain, and you can drop the include.

To check, send a real message from the service to a mailbox you control and view the original headers. Look at two places:

```text
Return-Path: <bounce-8123@mail.newsletter.example>
Authentication-Results: mx.receiver.example;
  spf=pass smtp.mailfrom=mail.newsletter.example;
  dkim=pass header.d=example.com;
  dmarc=pass header.from=example.com
```

Here SPF was checked against `mail.newsletter.example`, not `example.com`, so the include in your record does nothing for this stream. DMARC passes because of DKIM. Only remove the include after you confirm DKIM passes and aligns for that service.

### 3. Give a heavy sender its own subdomain

If a service must use your domain in the Return-Path, give it a subdomain such as `bounce.example.com` as a custom Return-Path, and publish a separate SPF record there:

```dns
bounce.example.com.  TXT  "v=spf1 include:mail.newsletter.example -all"
```

That name has its own budget of 10 lookups, and your main record loses the include. Relaxed alignment still matches, because `bounce.example.com` and `example.com` share an organizational domain. The setting for this is usually called custom Return-Path, custom bounce domain, or custom MAIL FROM in the sending service.

### 4. Replace `a` and `mx` with literal addresses

If `a` or `mx` points at servers that are yours and rarely change, list their addresses with `ip4` and `ip6` instead. Each swap saves one lookup, and it saves more if your `mx` is a set of hosts. If your web server does not send mail, drop `a` entirely. If your MX hosts only receive mail, which is common with hosted mailboxes, you may not need `mx` either.

Only do this for addresses you control. If the addresses can change without you knowing, a literal list will go stale.

### 5. Flatten, if you must

Flattening replaces includes with the IP ranges they resolve to. It works: `ip4` and `ip6` cost no lookups. The risk is that providers change their ranges without notice. A record you flattened by hand keeps listing the old addresses, and mail from the new ones starts failing with no error to point at the cause.

If you flatten, use tooling that re-resolves the includes on a schedule and republishes the record. Even then, prefer fixes 1 to 4. Flattened records also get long, so they may hit the 255-character limit on a single TXT string (see below).

## What not to do

- **Do not publish two SPF records.** Two `v=spf1` TXT records on one name give permerror. Merge them into one record. Splitting a long record into two does not double your budget.
- **Do not use `+all`.** It authorizes every server on the internet to send as your domain.
- **Do not use `ptr`.** It is slow, unreliable, and deprecated by RFC 7208. It also costs a lookup.
- **Do not keep dead includes "just in case."** Each one is a lookup you may need later.

## Long records and the 255-character limit

This is a separate problem from the lookup limit, and it often shows up at the same time. A single string inside a TXT record can hold at most 255 characters. A longer SPF record has to be split into several quoted strings within one record:

```dns
example.com.  TXT  "v=spf1 ip4:192.0.2.0/24 ip4:198.51.100.0/24 ip4:203.0.113.0/24 " "include:_spf.crm.example -all"
```

Receivers join the strings without adding spaces, so put the space at the end of one string or the start of the next. Many DNS hosts split the strings automatically. If yours does not, you will see an error when you save the record. Splitting strings is fine. Publishing a second record is not.

## Choosing the ending

The ending of your record tells receivers what to do with mail from servers you did not list:

| Ending | Meaning |
| --- | --- |
| `-all` | Fail: not authorized |
| `~all` | Softfail: probably not authorized |
| `?all` | Neutral: no statement |
| `+all` | Pass everything: never use this |

Once DMARC is at `p=quarantine` or `p=reject`, the choice between `-all` and `~all` matters much less, because DMARC decides what happens to the message. Either is fine. `?all` is weak and `+all` defeats the purpose of SPF.

## Where DNS Autopilot fits

The [free check](https://dnsautopilot.com/check) counts your SPF lookups against the limit of 10, checks how the record ends, and gives you the exact record to publish for each fix above. It needs no sign-up. It also looks at DKIM, DMARC, MX, MTA-STS, DNSSEC, and other records, and gives the domain a score from 0 to 100.

The count can drift on its own, so a one-time check is not enough. A free account re-checks your domain every day, which catches a provider pushing you over 10 before your mail fails. If you [send DMARC reports to DNS Autopilot](https://dnsautopilot.com/docs/setup), from Gmail or a report address, it also shows which senders fail SPF or DKIM, so you know which services to move to DKIM or a subdomain before you delete anything.

## FAQ

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

It is the maximum number of DNS-causing terms a receiver will process while evaluating one SPF record. The terms are `include`, `a`, `mx`, `ptr`, `exists`, and `redirect`, counted across all nested records. Going over gives a `permerror`, and SPF does not pass.

### What happens when SPF has too many DNS lookups?

SPF returns `permerror`, which counts as a failure to authenticate. DMARC then depends on aligned DKIM. If DKIM also fails, DMARC fails, and with `p=quarantine` or `p=reject` the message is sent to spam or rejected.

### Do ip4 and ip6 count toward the limit?

No. `ip4`, `ip6`, and `all` need no DNS query, so they cost nothing. The `exp` modifier is not counted either. This is why replacing an include with literal addresses reduces your count.

### Can I have two SPF records to get more lookups?

No. A domain must have exactly one SPF record, and two `v=spf1` TXT records on the same name give `permerror`. To get a bigger budget, move a sender to a subdomain with its own SPF record, since each name has its own 10 lookups.

### Is SPF flattening a good fix?

It works, but it is the least safe option. Providers change their IP ranges without notice, so a record you flattened by hand silently breaks. Remove unused includes, switch to DKIM, or use a subdomain first, and flatten only with tooling that re-resolves on a schedule.

### Why did my SPF record break when I changed nothing?

Includes count recursively, and the providers you include edit their own records. If a provider adds a nested include, your total goes up with no change on your side. Re-checking your record regularly is the only way to catch this early.

## Check your domain

DNS Autopilot's free check scores any domain from 0 to 100 across email authentication, mail servers, mail in transit, DNS security, and domain and web, and gives the exact record to publish for each fix: https://dnsautopilot.com/check. No sign-up. AI assistants and scripts can fetch https://dnsautopilot.com/check/{domain}.md for the same report as Markdown, for example https://dnsautopilot.com/check/example.com.md.

## Related guides

- [From DMARC p=none to p=reject: a safe step-by-step path](https://dnsautopilot.com/blog/dmarc-p-none-to-p-reject)
- [MTA-STS and TLS-RPT: how to require TLS for mail to your domain](https://dnsautopilot.com/blog/mta-sts-and-tls-rpt)
