---
title: "SPF Too Many DNS Lookups: How to Fix the 10 Limit"
description: "SPF returns permerror past 10 DNS lookups. Count yours with a Node script, see what common includes really cost, and get back under the limit."
source: "https://www.mailmark.dev/blog/spf-too-many-dns-lookups"
site: "Mailmark"
---

# SPF Too Many DNS Lookups: How to Fix the 10 Limit

"SPF too many DNS lookups" means that checking your SPF record takes more than 10 DNS-querying terms, the cap set by [RFC 7208](https://datatracker.ietf.org/doc/html/rfc7208#section-4.6.4). Past 10, receivers must stop and return `permerror`, so SPF fails for mail checked against that record. To fix it, count lookups across the whole include tree (not just your own record), remove includes you no longer use, drop `a`, `mx`, and `ptr` terms, and move third-party senders to their own subdomains, each of which gets a fresh budget of 10.

> **One SPF record, merged for you** Add a domain to Mailmark and it reads the SPF record you already publish and shows the single merged value to paste, so you never end up with a second record. SES bounces go through a mail subdomain with its own SPF record and its own lookup budget. [Add your domain](https://www.mailmark.dev/?utm_source=guide&utm_medium=seo&utm_campaign=spf-too-many-dns-lookups&utm_content=top)

## What counts as a DNS lookup

RFC 7208 is specific about which terms cost a lookup. The limit applies to the total across your record and everything it pulls in, not to the number of words in your TXT value.

| Term | Costs a lookup? | Notes |
| --- | --- | --- |
| `include:` | Yes | 1 for the include, plus every lookup inside the included record |
| `redirect=` | Yes | Plus every lookup inside the target record |
| `a` | Yes | Resolves the A or AAAA records of the domain |
| `mx` | Yes | Each MX host it returns may resolve to no more than 10 A or AAAA records |
| `exists:` | Yes | Common in macro-based records such as Salesforce's |
| `ptr` | Yes | [RFC 7208 section 5.5](https://datatracker.ietf.org/doc/html/rfc7208#section-5.5) says it "SHOULD NOT be published" |
| `ip4:`, `ip6:` | No | Free, no DNS query |
| `all` | No | Free |
| `exp=` | No | Only looked up after a fail, to build an explanation |

There is a second, less known limit. The RFC says receivers SHOULD allow no more than two **void lookups**, meaning queries that return no records or a nonexistent domain. Going past that is also a `permerror`. The usual cause is an include for a vendor that has since deleted its SPF record. [Microsoft documents](https://learn.microsoft.com/en-us/defender-office-365/email-authentication-spf-configure) the same rule: an include that doesn't resolve, or has no SPF record, returns `permerror` on its own.

## Count your lookups with a short Node script

Online checkers work, but a script you can rerun before every DNS change is more useful. Save this as `spf-count.mjs` and run it with Node 18 or newer. It has no dependencies.

```js
// spf-count.mjs: node spf-count.mjs example.com
import { resolveTxt } from "node:dns/promises";

const COUNTED = new Set(["include", "a", "mx", "ptr", "exists", "redirect"]);

async function getSpf(domain) {
  const txt = await resolveTxt(domain).catch(() => []);
  const records = txt
    .map((chunks) => chunks.join(""))
    .filter((r) => /^v=spf1(\s|$)/i.test(r));
  if (records.length > 1) throw new Error(`${domain} has ${records.length} SPF records`);
  return records[0] ?? null;
}

async function count(domain, depth = 0) {
  const pad = "  ".repeat(depth);
  const record = await getSpf(domain);
  if (!record) {
    console.log(`${pad}${domain}: no SPF record (void lookup)`);
    return 0;
  }
  let total = 0;
  for (const term of record.split(/\s+/).slice(1)) {
    const [, name = "", value = ""] = term.match(/^[+\-~?]?([a-z0-9]+)[:=]?(.*)$/i) ?? [];
    if (!COUNTED.has(name.toLowerCase())) continue;
    total += 1;
    console.log(`${pad}${domain}: ${term} +1`);
    const nested = ["include", "redirect"].includes(name.toLowerCase());
    if (nested && !value.includes("%")) total += await count(value, depth + 1);
  }
  return total;
}

const domain = process.argv[2];
const total = await count(domain);
console.log(`\n${domain}: ${total} of 10 lookups${total > 10 ? " (permerror)" : ""}`);
```

It joins multi-string TXT records the way [RFC 7208 section 3.3](https://datatracker.ietf.org/doc/html/rfc7208#section-3.3) requires (no spaces added), stops with an error if it finds two SPF records, and prints the tree so you can see which include is expensive. It deliberately keeps things simple: it does not expand macros like `%{i}`, and it does not count the address lookups behind `mx`. Treat its total as the floor.

## What common includes actually cost

Lookup costs change when vendors restructure their records, and many guides still repeat old numbers. Google's `_spf.google.com`, often quoted as costing 4 lookups, now publishes its IP ranges directly and costs 1. These totals come from running the script above against public DNS on October 31, 2026:

| You add | Total lookups it costs |
| --- | --- |
| `include:_spf.google.com` (Google Workspace) | 1 |
| `include:spf.protection.outlook.com` (Microsoft 365) | 1 |
| `include:amazonses.com` (Amazon SES) | 1 |
| `include:servers.mcsv.net` (Mailchimp) | 1 |
| `include:mail.zendesk.com` (Zendesk) | 1 |
| `include:sendgrid.net` (SendGrid) | 2 |
| `include:_spf.salesforce.com` (Salesforce) | 2 |
| `include:mailgun.org` (Mailgun) | 5 |

Rerun the script for your own includes rather than trusting any table, including this one.

## Worked example: a typical SaaS record at 15 lookups

Here is a realistic record for a founder who has added a tool every few months:

```txt
v=spf1 include:_spf.google.com include:amazonses.com include:mailgun.org include:sendgrid.net include:servers.mcsv.net include:_spf.salesforce.com include:mail.zendesk.com a mx ~all
```

Only nine terms, but the script shows 15 lookups:

```txt
example.com: include:_spf.google.com +1
example.com: include:amazonses.com +1
example.com: include:mailgun.org +1
  mailgun.org: include:_spf.mailgun.org +1
    _spf.mailgun.org: include:_spf1.mailgun.org +1
    _spf.mailgun.org: include:_spf2.mailgun.org +1
  mailgun.org: include:_spf.eu.mailgun.org +1
example.com: include:sendgrid.net +1
  sendgrid.net: include:ab.sendgrid.net +1
example.com: include:servers.mcsv.net +1
example.com: include:_spf.salesforce.com +1
  _spf.salesforce.com: exists:%{i}._spf.mta.salesforce.com +1
example.com: include:mail.zendesk.com +1
example.com: a +1
example.com: mx +1

example.com: 15 of 10 lookups (permerror)
```

Getting it back under 10, cheapest change first:

1. **Remove SendGrid.** The app moved to SES last year and nobody cleaned up. 15 becomes 13.
2. **Drop `a` and `mx`.** `a` authorizes the web host, which never sends mail. `mx` authorizes the inbound mail servers, which here are Google's and already covered by Google's include. 13 becomes 11.
3. **Move Mailgun to a subdomain.** Verify `mg.example.com` as the sending domain in Mailgun and publish `v=spf1 include:mailgun.org ~all` there. Those 5 lookups now count against the subdomain's own budget. 11 becomes 6.

The root record ends up at 6 lookups, with room for four more senders:

```txt
v=spf1 include:_spf.google.com include:amazonses.com include:servers.mcsv.net include:_spf.salesforce.com include:mail.zendesk.com ~all
```

Step 3 works because of how DMARC alignment is defined. SPF is checked against the envelope sender (the MAIL FROM, or Return-Path) domain, not the From address people see, and [RFC 7489 section 3.1.2](https://datatracker.ietf.org/doc/html/rfc7489#section-3.1.2) says that in relaxed mode, the default, the SPF domain and the From domain only need the same organizational domain. Mail with a Return-Path on `mg.example.com` and a From of `you@example.com` still aligns. [Microsoft recommends](https://learn.microsoft.com/en-us/defender-office-365/email-authentication-spf-configure) exactly this for bulk senders you don't control, for reputation reasons as well as the lookup budget.

Amazon SES has the same split built in. With a [custom MAIL FROM domain](https://docs.aws.amazon.com/ses/latest/dg/mail-from.html), SES mail is checked against a subdomain that carries its own `v=spf1 include:amazonses.com ~all` record. Mailmark sets this up on every domain using `mail.yourdomain.com`, so that check never touches your root budget. If SPF and DMARC are new to you, [how SPF, DKIM, and DMARC work together](/blog/understanding-spf-dkim-and-dmarc) covers alignment in more depth.

## When SPF flattening is worth it

Flattening means replacing an `include:` with the `ip4:` and `ip6:` ranges it resolves to. It cuts lookups to zero for that vendor, and it moves the job of tracking the vendor's IP changes from them to you. If a vendor adds a range and your flattened copy doesn't, their mail from the new IPs fails SPF with no warning.

[Microsoft's guidance](https://learn.microsoft.com/en-us/defender-office-365/email-authentication-spf-configure) is to flatten only vendors with stable, documented IP ranges, never `spf.protection.outlook.com` (its IPs change frequently), and to review flattened entries at least quarterly. Flattening also makes the record longer. Each TXT string is limited to 255 characters and longer values are split into several strings, and [RFC 7208 section 3.4](https://datatracker.ietf.org/doc/html/rfc7208#section-3.4) says the record SHOULD stay small enough that a DNS response for it fits in 512 octets.

| Your situation | Best fix |
| --- | --- |
| An include for a service you stopped using | Delete it |
| `a`, `mx`, or `ptr` in the record | Delete it, or replace with `ip4:` for a host that really sends |
| A marketing or transactional platform with a deep include tree | Move it to its own sending subdomain |
| A vendor that publishes a short, stable list of IPs | Flatten it, and put a quarterly review in the calendar |
| Microsoft 365 or another provider with changing IPs | Keep the include |
| Still over 10 after all of the above | Fewer services sending as the root domain, or a paid flattening service that updates the record automatically |

> **Catch a broken SPF record before your bounces do** Mailmark re-checks SPF, DKIM, DMARC, and blacklist status for every domain every 12 hours and shows the result on your dashboard, so an edit that breaks authentication shows up quickly. [Start monitoring free](https://www.mailmark.dev/?utm_source=guide&utm_medium=seo&utm_campaign=spf-too-many-dns-lookups&utm_content=middle)

## Troubleshooting SPF permerror

**The checker says 11 lookups, but most mail still passes.** Receivers evaluate the record left to right and stop at the first match, as [Microsoft explains](https://learn.microsoft.com/en-us/defender-office-365/email-authentication-spf-configure). Mail from an early include passes before the limit is reached, while mail from a sender near the end fails. That is why this error often looks intermittent. Fix the count anyway.

**Permerror with fewer than 10 lookups.** Look for one of these:

- Two TXT records starting with `v=spf1`. A domain [must not have more than one](https://datatracker.ietf.org/doc/html/rfc7208#section-3.2). Merge them into one.
- An include for a vendor whose SPF record no longer exists, which counts as a void lookup.
- A syntax error. Microsoft lists the common ones: a trailing period (`include:spf.protection.outlook.com.`), an equals sign instead of a colon (`include=`), or a space after the colon.

**DMARC still passes even though SPF has a permerror.** DMARC passes when [at least one of SPF or DKIM](https://datatracker.ietf.org/doc/html/rfc7489#section-4.2) passes and is aligned. An aligned DKIM signature carries the message. Don't treat that as a fix: [Google's sender guidelines](https://support.google.com/mail/answer/81126) require both SPF and DKIM from anyone sending 5,000 or more messages a day to Gmail.

**Microsoft 365 bounces say "The message required too many lookups" or "The message exceeded the hop count".** Microsoft documents both wordings as SPF lookup failures. Count lookups for the domain in the bounce's envelope sender, which may be a subdomain rather than your root domain.

**You fixed the record, but the checker still shows the old count.** Resolvers cache the old TXT record until its TTL expires. Wait out the TTL, then rerun the count. Microsoft recommends a TTL of at least 3600 seconds on SPF records, so plan on up to an hour.

If SPF passes and mail still lands in junk, the cause is usually elsewhere: see [why emails go to spam](/blog/why-emails-land-in-spam) for the other common causes.

## Checklist before you add a new sender

- Run the count first and write the current total down.
- Look up the new vendor's include and count its tree on its own.
- If it costs more than 2 lookups, give it a sending subdomain instead of the root.
- Edit the existing SPF record. Never publish a second one.
- Remove at least one unused include whenever you add one.
- Check the result with the free [email deliverability checker](/tools/email-deliverability-checker), which pulls your live SPF, DKIM, DMARC, MX, and blacklist status, then rerun the script once the TTL has passed.

For the full set of records a sending domain needs, including where SPF sits next to DKIM and DMARC, see [DNS records for email](/blog/dns-setup).

## Frequently asked questions

### Does an include count as one DNS lookup?

It costs one lookup for the include itself, plus every lookup inside the record it points to. An include whose record contains three more includes costs four in total.

### Do ip4 and ip6 mechanisms count toward the 10 lookup limit?

No. ip4, ip6, and all never trigger a DNS query, so they are free. Only include, a, mx, ptr, exists, and redirect count.

### Will an SPF permerror get my email rejected?

It depends on the receiver. Microsoft 365 can bounce the message with 'The message required too many lookups'. For DMARC, a permerror is simply not a pass, so the message still passes DMARC if it carries an aligned DKIM signature, and fails DMARC if it does not.

### Is SPF flattening safe?

Only for vendors with stable, documented IP ranges, and only if you re-check those ranges regularly. Microsoft says not to flatten its own include because its sending IPs change frequently. Moving senders to subdomains is the safer fix.

### Do subdomains share the root domain's 10 lookup limit?

No. SPF is checked against the domain in the envelope sender (MAIL FROM), and each domain or subdomain has its own record and its own budget of 10 lookups.

> **Run every product domain from one place** Mailboxes, 30-day warming, campaigns, and SPF, DKIM, and DMARC checks for all your domains in one dashboard, on SES in Mailmark's AWS account or your own. [Try Mailmark free](https://www.mailmark.dev/?utm_source=guide&utm_medium=seo&utm_campaign=spf-too-many-dns-lookups&utm_content=end)


Published 2026-10-31. Updated 2026-10-31. 7 min read.

---

Mailmark - email hosting and campaigns for your own domain. Canonical page: https://www.mailmark.dev/blog/spf-too-many-dns-lookups | [llms.txt](https://www.mailmark.dev/llms.txt) | [OpenAPI](https://www.mailmark.dev/openapi.json) | [sitemap](https://www.mailmark.dev/sitemap.xml)
