---
title: "Check your SPF record"
description: "Look up a domain's SPF record, count its DNS lookups against the limit of 10, follow every include, and see what each error means and how to fix it."
url: https://www.unitpost.com/tools/spf-checker
section: Free tools
updated: 2026-11-02
---
# Check your SPF record

SPF is a TXT record starting with v=spf1 that lists the servers allowed to send mail for a domain's Return-Path. While evaluating it, receivers allow at most 10 DNS lookups (include, a, mx, ptr, exists and redirect, nested includes counted) and 2 void lookups; past either limit SPF returns a permerror for every message. This checker follows every include, counts both, and flags syntax errors, duplicate records and risky all terms.

> Interactive tool: https://www.unitpost.com/tools/spf-checker. The lookups go from your browser to a public DNS-over-HTTPS resolver. Nothing is sent to Unitpost, and there's no sign-up.

## How SPF lookups are counted

> 

Every term that makes the receiver query DNS counts toward a limit of 10 per evaluation (RFC 7208 §4.6.4), and the count includes every term inside nested includes. Terms that carry their answer in the record cost nothing.

| Term | Lookups | Why |
| --- | --- | --- |
| `include:` | 1, plus every lookup in the included record | The receiver fetches and evaluates the other record. |
| `redirect=` | 1, plus every lookup in the target record | The other record replaces this one. |
| `a`, `a:host` | 1 | An address lookup for the host. |
| `mx`, `mx:host` | 1 | An MX lookup. More than 10 MX hosts behind one `mx` is a permerror on its own. |
| `ptr` | 1 | A reverse lookup of the sending IP. RFC 7208 says not to publish it. |
| `exists:` | 1 | An address lookup, usually on a macro. |
| `ip4:`, `ip6:` | 0 | The addresses are in the record. |
| `all` | 0 | Matches every sender; nothing to look up. |
| `exp=` | 0 | Fetched only after a fail, to explain it. |

An include can cost several lookups when the record it names has includes of its own. The include tree in the result shows the count for each record, so you can see which one is expensive.

## Void lookups

> 

A void lookup is a DNS query that comes back with nothing: the name doesn't exist (NXDOMAIN), or it exists with no record of the type asked for. RFC 7208 has receivers stop after two, and a third makes the result a permerror.

They come from includes of domains that no longer exist (a provider you left, a typo) and from `a:` or `mx:` terms naming hosts that were deleted. The fix is to remove the term; it authorizes nothing.

## Flattening an SPF record, and what to do instead

> 

Flattening replaces includes with the `ip4:` and `ip6:` ranges they currently resolve to. It brings the lookup count down, but it freezes a list the provider can change at any time.

- When a provider adds a range, mail from it fails SPF until you update your record.
- The record grows. RFC 7208 asks for a record whose DNS answer fits in 512 bytes, and one TXT string holds at most 255 characters, so a long record has to be split into several strings.
- Services that re-flatten automatically fix the staleness, but your SPF then depends on them publishing correctly.

Two changes usually bring the count down without flattening:

- Remove includes for services you no longer send through, and any include that appears twice.
- Give senders their own Return-Path subdomain. SPF is checked on the Return-Path domain, and each name has its own budget of 10 lookups. Providers that use a custom return-path subdomain publish SPF there, not in your apex record. Unitpost uses `unitpost.<your domain>`, so it adds nothing to your apex record.

## ~all or -all: softfail vs fail with DMARC

> 

With DMARC enforced, the choice matters less than it seems: DMARC acts on SPF and DKIM together, and a message that fails SPF but passes aligned DKIM still passes DMARC.

| Ending | Result for unlisted servers | Use it when |
| --- | --- | --- |
| `-all` | fail | The domain sends no mail (`v=spf1 -all`), or every sender is listed and you accept the forwarding risk below. |
| `~all` | softfail | The default for a domain with DMARC at quarantine or reject. |
| `?all` | neutral | Never on a sending domain; it asserts nothing. |
| `+all` or `all` | pass | Never; it authorizes every server on the internet. |

The catch with `-all`: some receivers reject on an SPF fail before they evaluate DKIM or DMARC (RFC 7489 §10.1). Forwarded mail always fails SPF, so `-all` can lose forwarded messages that DKIM would have carried. `~all` with an enforced DMARC policy still stops spoofing.

## Errors and how to fix them

### No SPF record found

Also reported as: SPF record not found; No SPF record; spf=none

No TXT record at this name starts with `v=spf1`, so receivers get an SPF result of none and can't tell which servers may send for it.

1. Check the right name. SPF is looked up on the Return-Path domain, which is often a provider's subdomain of yours; for Unitpost it is `unitpost.<your domain>`.
2. If this name sends mail, publish one TXT record at it: `v=spf1 include:<your provider's include> ~all`, using the include each provider documents.
3. If it sends no mail, publish `v=spf1 -all` so receivers can reject anything that claims to be from it.
4. Wait for the TTL and check again. Other causes are in [SPF record not found](/blog/troubleshooting/spf-record-not-found).

### Multiple SPF records found

Also reported as: More than one SPF record; SPF permerror: multiple records; Found multiple SPF records

RFC 7208 allows one `v=spf1` record per name. With two or more, receivers return a permerror and SPF fails for every message, not just mail from one of the providers.

1. List what DNS serves: `dig TXT example.com +short`.
2. Merge the terms into one record with a single `v=spf1` at the start and a single `all` at the end: `v=spf1 include:_spf.one.example include:_spf.two.example ~all`.
3. Delete the other records and check again.

### SPF syntax error

Also reported as: Invalid SPF record; SPF permerror: unknown mechanism; SPF record is not valid

A term isn't valid SPF: an unknown mechanism, `=` where a mechanism takes `:`, a host name in `ip4:`, an invalid address, or a second `redirect=`. Receivers return a permerror for the whole record, so SPF fails for every message.

1. Read the result line: it quotes the term and says what's wrong with it.
2. Correct the term. `include=_spf.example.com` becomes `include:_spf.example.com`; `ip4:mail.example.com` becomes `a:mail.example.com`; `ip4:203.0.113.300` needs a real address such as `ip4:203.0.113.30`.
3. Separate terms with single spaces and check again after the TTL.

### SPF permerror: too many DNS lookups

Also reported as: Too many included lookups; SPF record exceeds 10 DNS lookups; PermError: too many DNS lookups

Evaluating the record takes more than 10 DNS lookups once nested includes are counted. Receivers stop at 10 and return a permerror once a message's check reaches the 11th, so mail from senders listed late, or not listed at all, fails SPF. The same finding covers an `mx` term whose domain has more than 10 MX hosts, which RFC 7208 also makes a permerror.

1. Find the expensive records in the include tree; each shows its own lookup count.
2. Remove includes for services you no longer send through, and any include that appears twice.
3. Replace `a` and `mx` terms with the `ip4:` and `ip6:` addresses they point at, when those are stable addresses you control.
4. Move bulk or marketing senders to their own Return-Path subdomain, which has its own budget of 10.
5. Flatten provider includes into IP ranges only as a last resort; see [flattening](/tools/spf-checker#flattening).

### SPF record is close to the 10-lookup limit

Also reported as: 9 of 10 SPF lookups used; SPF lookup count near limit

The record uses 9 or 10 of the 10 lookups receivers allow. It passes today, but one more include, or a provider adding a nested include to its own record, tips it into a permerror that fails every message.

1. Remove includes for services you no longer use.
2. Move a sender to its own Return-Path subdomain before you add the next provider.
3. Check again after any provider tells you to change your SPF record.

### SPF void lookup limit exceeded

Also reported as: Too many void DNS lookups; SPF permerror: void lookup limit; More than 2 void lookups

A void lookup returns nothing: the name doesn't exist, or has no record of the type asked for. Receivers allow 2 per evaluation and return a permerror on the third. One or two show as a warning, because each is a term that authorizes nothing.

1. Find the terms that point nowhere: includes of domains that are gone, `a:` or `mx:` hosts that were deleted, or a typo in a name.
2. Delete them, or correct the name.
3. Check again.

### A null DNS lookup was found for include

Also reported as: SPF void lookup; Include domain does not exist; SPF include returns NXDOMAIN

An `include:` or `redirect=` names a domain that doesn't exist. It counts as a void lookup, and because the included record can't be evaluated, receivers return a permerror for the whole record.

1. Copy the include again from the provider's current setup instructions; a typo, or an include the provider retired, is the usual cause.
2. If you no longer send through that provider, delete the term.
3. Check again.

### No valid SPF record for included domain

Also reported as: SPF include has no SPF record; Include target missing v=spf1; permerror: include domain has no SPF record

The domain named in `include:` or `redirect=` exists but publishes no `v=spf1` record. RFC 7208 makes that a permerror once a message's check reaches it, so mail from senders not matched earlier in the record fails SPF.

1. Check you used the provider's SPF include, not its website or another of its host names.
2. If you no longer send through that provider, delete the term.
3. If the include is one of yours, such as `include:_spf.example.com`, publish the SPF record at that name.

### SPF include loop

Also reported as: SPF record includes itself; Recursive SPF include; SPF redirect loop

An include or redirect leads back to a domain already being evaluated, directly or through another record. Evaluation can never finish, and receivers end it with a permerror.

1. Remove any `include:` of the domain's own name; it adds nothing.
2. If two of your records include each other, keep the include in one direction only.
3. Check again.

### Found duplicate include on SPF path

Also reported as: Duplicate SPF include; Same include appears twice

The same include is reached more than once while evaluating the record: listed twice, or also included by another record you include. Each copy spends lookups and authorizes nothing new.

1. Delete the extra copy from your own record.
2. If a provider's record already includes it, remove yours.

### SPF record uses +all

Also reported as: SPF allows all senders; +all mechanism; Permissive SPF record

`+all`, or `all` with no qualifier, matches every server on the internet, so anyone can send mail that passes SPF for this domain. So does an include whose record ends in `+all`, and an `exists:` with a fixed name that resolves: it matches whoever sends.

1. List your real senders before the end of the record.
2. Replace `+all` with `~all`: `v=spf1 include:<your provider's include> ~all`.

### SPF record ends in ?all (neutral)

Also reported as: Neutral SPF policy; SPF ?all

`?all` asserts nothing about servers the record doesn't list: their mail gets a neutral result, which receivers treat much like having no SPF.

1. Make sure every service that sends as this domain is listed.
2. Change the ending to `~all`, and to `-all` later if you want; see [softfail vs fail](/tools/spf-checker#softfail-vs-fail).

### SPF record has no all mechanism

Also reported as: Missing all mechanism; SPF record does not end with all

Without an `all` or a `redirect=`, servers the record doesn't list get the default result, neutral, so the record never fails anything.

1. End the record with `~all` or `-all`: `v=spf1 include:<your provider's include> ~all`.

### SPF record ends in ~all (softfail)

Also reported as: SPF softfail; ~all vs -all

`~all` marks mail from unlisted servers as suspicious without failing it outright, and leaves the decision to DMARC. That is a sound setting when DMARC is enforced, so it is reported as a note, not a problem.

1. Keep `~all` if your DMARC policy is quarantine or reject.
2. Move to `-all` only once every legitimate sender is listed, and read [softfail vs fail](/tools/spf-checker#softfail-vs-fail) for the forwarding trade-off.

### SPF record uses the ptr mechanism

Also reported as: ptr mechanism deprecated; Avoid ptr in SPF

`ptr` makes the receiver look up the sending IP's host name and then confirm it with a forward lookup. RFC 7208 says not to publish it because it is slow and unreliable, and some receivers skip it, so mail it was meant to cover can fail.

1. Replace `ptr` with the `ip4:` and `ip6:` ranges, or the `a:` host, of the servers it was meant to cover.
2. Check again.

### SPF redirect ignored because of all

Also reported as: redirect= used with all; SPF redirect modifier ignored

RFC 7208 §6.1 has receivers ignore `redirect=` when the record contains an `all` mechanism anywhere, so the record you redirect to is never used.

1. To hand the whole policy to another record, remove the `all`: `v=spf1 redirect=_spf.example.com`.
2. To keep the policy here, remove the `redirect=` and list the senders in this record.

### SPF mechanisms after all are ignored

Also reported as: Terms after all; include after ~all

Receivers read terms left to right and stop at `all`, which matches every sender. Anything after it is never evaluated, so senders listed there aren't authorized.

1. Move the terms in front of `all`: `v=spf1 include:_spf.one.example include:_spf.two.example ~all`.
2. Check again.

### SPF record uses macros

Also reported as: SPF macro %{i}; exists:%{i} in SPF

A term contains a macro such as `%{i}` (the sending IP) or `%{d}` (the domain), which the receiver fills in for each message. It counts as one lookup, but a checker with no real message can't follow it, so this check doesn't call the record verified and the domain grade shows SPF as partly checked, at no cost. Macros are legitimate, and some large senders publish them with `exists:`.

1. Nothing to fix if you added it on purpose or copied it from a provider's instructions.
2. If you don't recognize it, find which service it belongs to before removing it; it may be what authorizes that service's mail.

### SPF record has extra quotes or text before v=spf1

Also reported as: SPF record does not start with v=spf1; Quoted SPF record

A TXT record contains `v=spf1` but doesn't start with it, usually because literal quote marks or a stray character were pasted into a DNS editor that adds its own quotes. Receivers only accept records that begin with `v=spf1`, so they ignore this one.

1. Open the record in your DNS editor and remove the extra quote marks or leading characters.
2. Check what DNS serves: `dig TXT example.com +short` should print the record inside one pair of quotes, which dig adds itself.
3. Check again.

## Run this check without a browser

The page runs these lookups in the browser. An agent or script can run the same ones over public DNS-over-HTTPS (JSON answers; Cloudflare's `https://cloudflare-dns.com/dns-query` works too, with `accept: application/dns-json`):

1. Read the record: `GET https://dns.google/resolve?name=example.com&type=TXT` and keep the answers that start with `v=spf1`. None means no SPF; more than one is a permerror.
2. Follow every `include:` and `redirect=` target the same way. Count `include`, `a`, `mx`, `ptr`, `exists` and `redirect` across the whole tree: more than 10 is a permerror. A lookup that returns nothing (NXDOMAIN or no answer) is a void lookup, and more than 2 is a permerror. Count `a` and `mx` host lookups per address family: a receiver connected over IPv4 looks up only A records, over IPv6 only AAAA.
3. For `mx`, query `type=MX`, then `type=A` and `type=AAAA` for each host: more than 10 hosts, or more than 10 addresses of one family on a host, is a permerror.
4. A term with a macro (`%{i}`, `%{d}`) depends on the message and can't be followed from DNS alone: report it as unchecked, not as a pass.
5. Read the final `all` (after any redirect): `-all` fails unlisted servers, `~all` softfails them, `?all` and a missing `all` are neutral, and `+all` passes everyone. An include whose record ends in `+all` also passes everyone, and so does a pass-qualified `exists:` with a fixed name (no macro) that resolves.

For a sending domain in a Unitpost workspace, the MCP tool `email_domains_diagnose` checks every record Unitpost expects, on each of the domain's nameservers.

## FAQ

### How many DNS lookups does SPF allow?

Ten per evaluation, counting `include`, `a`, `mx`, `ptr`, `exists` and `redirect`, including those inside nested includes. `ip4`, `ip6` and `all` don't count. Past ten, receivers return a permerror and SPF fails.

### Can a domain have two SPF records?

No. A name may have one `v=spf1` record, and two is a permerror that fails SPF for every message. Merge them into one record with several includes.

### Should SPF end in ~all or -all?

Use `~all` on a sending domain with an enforced DMARC policy, and `-all` on domains that send no mail. Some receivers reject on an SPF fail before checking DKIM, which can cost you forwarded mail.

### Does SPF check the From address?

No. SPF checks the envelope sender, the Return-Path domain. DMARC is what ties it to the From address, by requiring the two domains to align.

### Do subdomains inherit the SPF record?

No. SPF is looked up on the exact name, so a subdomain that sends mail needs its own record. DMARC is the one that falls back to the parent domain.

### What is an SPF permerror?

A permanent error: the record can't be evaluated as published, because of two records, a syntax error, too many lookups or a broken include. DMARC counts it as SPF not passing, for every message.

## Related

- [Check your DMARC record](https://www.unitpost.com/tools/dmarc-checker): Look up a domain's DMARC record at _dmarc, see the policy receivers apply, check the report addresses, and get the fix for each problem found.
- [Look up a DKIM record](https://www.unitpost.com/tools/dkim-checker): Look up a DKIM public key by selector and check its type, size and flags. Leave the selector empty to try common ones, and see the fix for each problem.
- [Generate a DMARC record](https://www.unitpost.com/tools/dmarc-generator): Build a DMARC record: pick a policy, a subdomain policy and a report address, then copy the value and publish it as a TXT record at _dmarc on your domain.
