# SPF Record Checker

Fetch and validate the SPF (RFC 7208) record of a domain. Finds the single v=spf1 TXT record (flags none or several), parses every term, then recursively resolves include, redirect, a, mx and exists to count DNS lookups (limit 10) and void lookups (limit 2) across the whole tree, as receivers do. Returns the record, final policy (-all, ~all, ?all, +all), the evaluation tree, and issues: syntax errors, includes that publish no SPF record or do not exist, loops, more than 10 lookups, +all, deprecated ptr, oversize records. Walks at most 60 DNS queries.

**Not for:** Generating a record (use spf_generate) or checking DMARC (use dmarc_check).

- Tool id: `spf-checker`
- Category: email
- Execution: network
- MCP tool `spf_check` at https://lookupowl.com/mcp
- REST `GET https://lookupowl.com/api/v1/tools/spf-checker` (or POST a JSON body)
- OpenAPI: https://lookupowl.com/api/openapi.json

## Input

| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| `domain` | string | yes |  | Domain whose SPF record to check, e.g. example.com. A URL or e-mail address is reduced to its domain; Unicode names are converted to Punycode. max length 253 |

## Output

Results are wrapped in an envelope: `tool`, `version`, `requestId`, `data`, `summary`, `meta` (cache status, timing), `permalink`. `data` has these fields:

| Field | Type | Description |
|---|---|---|
| `domain` | string |  |
| `status` | "ok" \| "warning" \| "error" \| "missing" \| "nxdomain" \| "dns-error" | ok = valid, warning = valid with warnings, error = receivers will return a permanent error (permerror), missing = no SPF record, nxdomain = the domain does not exist, dns-error = the resolver returned SERVFAIL or REFUSED. |
| `valid` | boolean | A record exists and a receiver can evaluate it (no permerror conditions). |
| `record` | string | The SPF record text (TXT strings joined). |
| `records` | string[] | Every TXT record that starts with v=spf1 (more than one is an error). |
| `terms` | string[] | The record's terms, normalised. |
| `length` | integer | Record length in bytes. min -9007199254740991, max 9007199254740991 |
| `policy` | object |  |
| `lookups` | object |  |
| `voidLookups` | object |  |
| `authorized` | object | Number of ip4 / ip6 mechanisms across the tree. |
| `tree` | object[] | Every record visited, depth first. |
| `issues` | object[] |  |
| `notes` | string[] |  |
| `dnsQueries` | integer | min -9007199254740991, max 9007199254740991 |
| `capped` | boolean | A query, lookup or depth cap stopped the walk. |
| `incomplete` | boolean | The deadline passed before every DNS query finished. |
| `ttl` | integer | min -9007199254740991, max 9007199254740991 |

## Examples

### SPF of google.com

```json
{"domain":"google.com"}
```

```bash
curl 'https://lookupowl.com/api/v1/tools/spf-checker?domain=google.com'
```

Web page: https://lookupowl.com/spf-record-checker?q=google.com

### SPF of github.com

```json
{"domain":"github.com"}
```

```bash
curl 'https://lookupowl.com/api/v1/tools/spf-checker?domain=github.com'
```

Web page: https://lookupowl.com/spf-record-checker?q=github.com

### SPF of example.com (null sender policy)

```json
{"domain":"example.com"}
```

```bash
curl 'https://lookupowl.com/api/v1/tools/spf-checker?domain=example.com'
```

Web page: https://lookupowl.com/spf-record-checker?q=example.com

## Web pages

- https://lookupowl.com/spf-record-checker

## Limits and caching

- Cost: 6 units for a live lookup, 1 unit for a cached one. Budgets are per network and published in the `RateLimit` response headers.
- Cache: the lifetime follows the data (for DNS, the record TTL, within a few minutes). Add `fresh=true` (MCP argument or query parameter) to look again; it costs 3 units per unit and is limited to 10 per hour.
- Timeout: 12 s.

## Related tools

- [SPF Record Generator](https://lookupowl.com/tools/spf-generator.md): Build a valid SPF record from your mail providers and IP addresses, with a lookup-limit check.
- [DMARC Record Checker](https://lookupowl.com/tools/dmarc-checker.md): Look up and validate a domain's DMARC policy, report addresses and alignment settings.
- [DKIM Record Checker](https://lookupowl.com/tools/dkim-checker.md): Find and check a domain's DKIM public keys, even when you do not know the selector.
- [IP and Domain Blacklist Checker](https://lookupowl.com/tools/blacklist-checker.md): Check an IP address, a mail server or a domain against live spam blocklists, with how to get delisted.
- [DMARC Record Generator](https://lookupowl.com/tools/dmarc-generator.md): Build a valid DMARC record, with report authorization records and a step-by-step rollout plan.
- [DNS Lookup](https://lookupowl.com/tools/dns-lookup.md): Look up A, AAAA, MX, NS, TXT, CNAME, SOA, CAA, DS and DNSKEY records from a public resolver or the authoritative nameserver.

## Guide: SPF record checker

See who is allowed to send mail for a domain, and count the DNS lookups against the limit of 10.

### What an SPF check tells you

The checker finds a domain's SPF record and reads it the way a receiving mail server does. It tells you whether the record is valid, which senders it allows, what it says about everyone else and how many of the 10 allowed DNS lookups it uses. A record that breaks the rules, or needs more than 10 lookups, stops working, and the domain loses a main defence against forged mail.

SPF (Sender Policy Framework) is a TXT record in which a domain lists the servers allowed to send its email. A receiving server compares the IP address that delivered a message with that list. SPF checks the hidden envelope sender, also called the return path, and not the From address people see.

### How to check a domain

1. **Enter the domain**: Use the domain in your email addresses, for example `example.com`. An email address or a web address is reduced to its domain.
2. **Read the verdict and the meter**: The top tile says whether the record is valid. A meter shows how many of the 10 lookups it uses, and the policy tile shows how the record ends. The Evaluation tab lists every record the check visited, with the lookups each one costs.
3. **Fix, publish and check again**: Edit the record at your DNS host, or build a new one with the [SPF generator](https://lookupowl.com/spf-record-generator). Results are kept for up to five minutes, so use Check again after you publish.

> **In Claude, ChatGPT or Cursor** Connect the LookupOwl MCP server and ask: “Does the SPF record for example.com go over the lookup limit?” The assistant calls `spf_check` and explains the errors in plain language.

### How to read an SPF record

An SPF record is a list of terms read from left to right. The first term that matches the sending server decides the result. A typical record looks like this:

```text
v=spf1 ip4:203.0.113.0/28 include:_spf.google.com include:sendgrid.net -all
```

| Term | Meaning | Counts as a DNS lookup? |
|---|---|---|
| `ip4:` / `ip6:` | Allows an address or a network directly. | No |
| `include:domain` | Also accepts whatever that domain's SPF record allows. | Yes, plus the lookups inside that record |
| `a` / `mx` | Allows the domain's own A/AAAA or MX hosts. | Yes, one each. The address lookups behind an `mx` are not counted again, but an `mx` with more than 10 hosts is an error |
| `exists:` | Matches if the named DNS name has an A record. Used with macros. | Yes |
| `redirect=domain` | Uses another domain's record instead of this one. Ignored if the record also has `all`. | Yes |
| `ptr` | Deprecated reverse-DNS match. Avoid it. | Yes |
| `all` | Matches every sender. Put it last: terms after it are never read. | No |

| Qualifier | Result on a match | Typical use |
|---|---|---|
| `+` (default) | Pass | Allowed senders |
| `-` | Fail | `-all`: ask receivers to reject everyone not listed |
| `~` | Softfail | `~all`: accept the mail but mark it as suspicious. Common while testing. |
| `?` | Neutral | `?all`: make no statement. It gives no protection. |

The lookup count is the worst case. LookupOwl adds up every term that needs a lookup, through the whole tree of includes, as if every term were evaluated. A real receiver stops at the first match, but a record has to work for every sender. Terms with macros, such as `exists:%{i}._spf.example.com`, depend on who is sending, so they are counted but not expanded.

### Problems the checker finds

- **More than 10 DNS lookups.** The most common failure. Over the limit, receivers return a permanent error (permerror) and ignore the record. The checker warns from 8 lookups, and its Evaluation tab shows which include costs the most. The questions below explain the limit and how to get under it.
- **More than 2 void lookups.** A void lookup is a query that returns nothing, such as an `a` host that does not exist. RFC 7208 recommends that receivers stop at two and treat more as a permanent error, so the checker reports it as an error. Remove the terms that point at empty names.
- **More than one SPF record.** Two records that start with `v=spf1` cause a permanent error, and receivers ignore both.
- **An include that fails.** An `include:` or `redirect=` that points at a domain with no SPF record, or at a domain that does not exist, is a permanent error. So is an include loop. Check the spelling, or remove the term.
- **`+all` or `?all`.** `+all` allows the whole internet to send as the domain. `?all` says nothing. Both defeat the purpose.
- **No `all` term.** Without a final `all` or a `redirect`, unlisted senders get a neutral result. The checker mentions it as a hint.
- **Terms after `all`.** They are never read. Move `all` to the end.
- **An unknown term.** A misspelled mechanism such as `includes:`, or an `ip4:` with an invalid address, makes the record unusable. A record that does not start with `v=spf1` and a space, such as `v=spf 1`, is not an SPF record at all, so the checker reports none.
- **Record too long.** One DNS string holds at most 255 bytes, so longer records are split into several strings. Above about 450 bytes the checker warns, because the whole answer may no longer fit in one 512-byte UDP packet (RFC 7208 section 3.4). Larger answers often work anyway, but not everywhere.
- **The `ptr` mechanism.** It is slow and unreliable, and RFC 7208 discourages it.

> **SPF alone is not enough** Forwarding breaks SPF, because the message arrives from the forwarder's address. Pair SPF with DKIM and a [DMARC policy](https://lookupowl.com/dmarc-checker). The guide to [SPF, DKIM and DMARC](https://lookupowl.com/learn/spf-dkim-dmarc-explained) shows how they fit together.

### How the check works

LookupOwl asks DNS for the domain's TXT records and keeps those that start with `v=spf1`. It parses the record strictly, then follows every `include` and `redirect` through DNS and looks up the addresses and mail hosts behind `a`, `mx` and `exists` terms. It counts the lookups and the empty answers across the whole tree, using the rules of RFC 7208.

The walk is kept polite to other people's DNS. It asks at most 60 questions, stops counting at 25 lookups, follows includes up to 12 levels deep and lists up to 40 records. If one of these limits stops it, or a deadline passes, the result says the numbers are lower bounds. Results are kept for up to five minutes.

```bash
dig +short TXT example.com | grep spf1
nslookup -type=TXT example.com
```

### Standards

- **RFC 7208**: the SPF specification, with the limit of 10 lookups (section 4.6.4), the recommended limit of two void lookups, the qualifiers and the macros.
- **RFC 5321**: SMTP, which defines the envelope sender that SPF checks.
- **RFC 9989**: DMARC, which builds on SPF alignment and replaces RFC 7489.
- **RFC 6376**: DKIM, the signature mechanism that complements SPF.

### Frequently asked questions

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

RFC 7208 lets a receiver follow at most 10 DNS-querying terms while it checks one SPF record. The terms are `include`, `a`, `mx`, `ptr`, `exists` and `redirect`, and lookups inside included records count too. Go over 10 and SPF returns a permanent error, which many receivers treat as a failure, so your own mail can fail SPF.

**How do I fix 'too many DNS lookups'?**

Start with the include that costs the most, which the Evaluation tab shows. Remove includes for services you no longer use, drop `ptr`, and replace `a` or `mx` with fixed `ip4` or `ip6` ranges when you control the hosts. Send bulk mail from a subdomain with its own SPF record. Flattening includes into IP lists is a last resort, because providers change their addresses and a stale list silently blocks real mail.

**Can a domain have two SPF records?**

No. Two records that start with `v=spf1` produce a permanent error, and receivers ignore both. Combine all the terms into a single record. If a service tells you to add its own SPF record, add its `include:` to your existing one instead.

**Should I use -all or ~all?**

Start with `~all` while you confirm that every sender is listed, then move to `-all` if you want a strict failure. With a DMARC policy of `quarantine` or `reject` in place, receivers largely follow DMARC, so `~all` is common and enough. `-all` can make a receiver reject a message before DMARC even looks at it.

**Do I need SPF if I use DKIM?**

Yes, publish both. DMARC passes when either one passes with alignment, but having both is more robust. DKIM survives plain forwarding better, while SPF needs no signing setup and many receivers still use it as a signal. With only one of them, a single mistake can make DMARC fail.

**Why does my SPF pass but mail still goes to spam?**

SPF is only one signal. Sender reputation, message content, DKIM, DMARC, reverse DNS and complaint rates all matter too. Check your [DMARC record](https://lookupowl.com/dmarc-checker), and check the [reverse DNS](https://lookupowl.com/reverse-dns-lookup) of the IP address that sends your mail. Also check that SPF passes for the domain in the return path, which can differ from your From domain.

**What does 'SPF permerror' mean?**

It means a permanent error: the record cannot be evaluated at all. The usual causes are a syntax mistake, two SPF records, an include that does not exist or more than 10 lookups. Receivers cannot use the record, so it gives no protection until you fix it. Retrying never helps, unlike a temporary error (temperror).

**Why does the checker say 'at least' before the lookup count?**

Because it could not count everything. A term with a macro depends on who is sending, so it is counted but not expanded. The walk may also have hit its limits or run out of time. The true number is at least the one shown, so if it is already over 10, the record needs fixing.
