Skip to content

SPF Checker

How the checker expands an SPF record through every include and redirect, counts the DNS lookups receivers allow, grades every term, and builds a flattening plan that replaces only the includes it must.

Open Markdown

The tool page is tamperbox.dev/spf-checker. It opens on a live check of tamperbox.dev; replace that domain to inspect your own. This page is the long version: how the record is expanded, every rule applied to it, what each finding means for mail delivery, and how to fix and verify each one.

Where processing happens: a domain is sent to the Tamperbox server, which asks public DNS for its TXT records and follows every include and redirect the same way, one lookup each. A pasted record is parsed in your browser; only the include and redirect names it contains are looked up. Nothing is stored.

SPF lets a domain publish which servers may send mail for it. Receivers read the record, follow its includes, and allow at most 10 DNS lookups while doing so. The tool answers:

  1. How many lookups does this record cost? Every include, redirect, a, mx, ptr and exists counts, through every nested include.
  2. Is the record valid? Every term is parsed against RFC 7208; one unknown term makes the whole record a permanent error.
  3. Which includes could be flattened, and what would the record look like? Only includes that publish plain address ranges are candidates.
  4. What else is wrong with it? A missing or permissive all, ptr, duplicates, void lookups, a record over 255 characters.

It does not evaluate a sender address. A real receiver stops at the first matching mechanism, so mail matched early can still pass a record that is over the limit.

example.com. When you stop typing, or on Enter, the server reads the TXT records at the domain and the browser keeps the ones that start with v=spf1. Each include and redirect target is looked up the same way, ten levels deep at most and 100 names in total.

A pasted web address or email address, such as https://example.com/contact or postmaster@example.com, is cleaned to the domain it holds, and the page says what it kept. Text with no usable domain stays as it was, and the field explains what is wrong. A domain with non-ASCII labels is converted to A-labels before it is looked up.

Paste the record itself, with or without the quotes DNS tools print: v=spf1 include:_spf.example.net ~all. It is parsed in the browser as the root of the tree, shown as “Your record”. Its includes are still looked up through the server, so the count covers what a receiver would read. Two records on two lines are treated as two published records, which is itself a finding.

Drop a .txt file anywhere on the page, or use Open file. It is read in the browser.

The verdict names the outcome: the budget exceeded, the record broken, something to check, or the record checked. The line under it carries the count and, when the record is over the limit, how far the flattening plan brings it. The chips show the count against the limit, the all mechanism, whether every include was looked up, and the number of findings; each one is a button that scrolls to the section that explains it.

Everything found, worst first. Two or more passes fold into one line. A budget finding carries a “See the plan” button. The finding reference lists them all.

Links that carry a value derived from this result into another tool: the biggest include on its own (this page again, with that domain), the DMARC policy that uses the record, who receives mail for the domain, and the network behind the first address the record lists. Each link opens the destination with the value already in its box; nothing is put in the URL.

History logs every Break it and Heal it change with what it replaced; Revert undoes the newest, Reset input returns to what you entered. Heal it offers the repairs the tool found: the flattened record, merging two records into one, changing +all to ~all, adding ~all, removing ptr, removing an invalid address mechanism, correcting a misspelt include, removing duplicates.

The count against the limit, and one slot per lookup in the order a receiver makes them, each slot named by the term and the record it came from. The tenth slot is marked; anything beyond it is red. The note under the slots says how many lookups are left, or that an evaluation reaching the eleventh stops there.

The record and every include and redirect under it, each with the record it publishes and how many lookups it costs with everything beneath it. A branch that could not be evaluated shows the reason in red.

Shown only when the record is over the limit. Each top-level include is marked Flatten or Keep with the reason, the flattened record is shown split into 255-character strings with a copy button, and the note says how many characters and strings it has.

Every term with its meaning and whether it costs a lookup; an unknown term is marked.

Every rule below names the section of RFC 7208 it comes from. Rules that decide a FAIL quote the normative sentence behind them. Rules marked “Tamperbox” are our own recommendations, not RFC requirements.

The server asks public DNS for the TXT records at the name, as section 4.4 prescribes (“querying for type TXT only”), and the browser keeps the answers that start with v=spf1 followed by whitespace or the end of the record. Section 4.5 decides the outcome: “If the resultant record set includes no records, check_host() produces the “none” result. If the resultant record set includes more than one record, check_host() produces the “permerror” result.”

  • No TXT record at the name (NXDOMAIN or an empty answer) is FAIL, “No record at domain.”
  • TXT records, none of them SPF, is FAIL, “No SPF record at this name.”
  • Two or more records is FAIL, “N SPF records: receivers return permerror.” The first record is still parsed and expanded, so the rest of the result describes it.
  • A record published as several quoted strings is joined first, as section 3.3 requires: “the record MUST be treated as if those strings are concatenated together without adding spaces.”

The record is split on whitespace. Each term after v=spf1 is a mechanism from section 4.6.1 with an optional +, -, ~ or ? qualifier, or a name=value modifier. Names are compared without case, because “mechanism and modifier names are case-insensitive” (section 4.6.1). Every syntax error is FAIL, because section 4.6 says: “if there are any syntax errors anywhere in the record, check_host() returns immediately with the result “permerror”“.

Rule Finding RFC section
A mechanism name outside the eight in 4.6.1 Unknown term X 4.6.1
ip4 value that is not four octets from 0 to 255 without leading zeros Invalid IPv4 address in X 5.6
ip6 value that does not parse as an IPv6 address Invalid IPv6 address in X 5.6
IPv4 prefix over /32 (also reported as an invalid prefix) Prefix too long in X 5.6
IPv6 prefix over /128, the dual /n//n form on ip4 or ip6, a prefix on any mechanism other than ip4, ip6, a and mx Invalid prefix in X 5.6
include or exists without a domain include needs a domain, exists needs a domain 5.2, 5.7
redirect= or exp= with an empty value redirect needs a domain, exp needs a domain 6.1, 6.2
A value attached to all Invalid all mechanism X 5.1
The same modifier name twice Duplicate X modifier 6

The duplicate rule follows section 6: “These two modifiers MUST NOT appear in a record more than once each. If they do, then check_host() exits with a result of “permerror”.” The tool applies it only to redirect and exp; repeated unknown modifiers are ignored for validity.

An unknown modifier is CHECK, “Unknown modifier X”. Section 6 says “Unrecognized modifiers MUST be ignored no matter where, or how often, they appear in a record”, so it never breaks the record; the tool reports it because a misspelt redirect is silently ignored the same way (Tamperbox).

Section 4.6.2: “Each mechanism is considered in turn from left to right.” The tool walks the terms in that order and stops at the first all, following section 5.1: “Mechanisms listed after “all” MUST be ignored.” Terms after it are not counted. redirect is followed only when the record has no all, after every mechanism, because section 6.1 says: “Any “redirect” modifier MUST be ignored if there is an “all” mechanism anywhere in the record.” The lookup budget numbers the slots in this order, descending into each include as it is reached.

Section 4.6.4 lists the terms that cause DNS queries: include, a, mx, ptr, exists and redirect. Each costs one; ip4, ip6, all and exp cost nothing. The rule: “SPF implementations MUST limit the total number of those terms to 10 during SPF evaluation, to avoid unreasonable load on the DNS. If this limit is exceeded, the implementation MUST return “permerror”.”

The count is one total for the whole tree, because section 4.1 says the limit “must be tracked as a single global limit for all evaluations, not just for a single instance of a recursive evaluation.” An include reached twice through different branches is counted twice.

Total Severity Title RFC section
11 or more FAIL N of 10 DNS lookups 4.6.4
9 or 10 CHECK N of 10 DNS lookups none (Tamperbox early warning)
8 or fewer PASS N of 10 DNS lookups 4.6.4

The total is what a receiver spends on a sender that matches nothing, since then every term is evaluated. For a sender matched earlier, “processing ends” at the match (section 4.6.2) and fewer lookups are made.

Section 4.6.4 also limits lookups that return nothing: “SPF implementations SHOULD limit “void lookups” to two.” and “Exceeding the limit produces a “permerror” result.” The tool counts a void when the TXT query for an include or redirect target returns NXDOMAIN or no TXT records at all. A target that publishes TXT records but no SPF record is not void, though it still fails (see below). More than two voids is FAIL, “N void lookups”. The a, mx and exists queries are never made, so their voids are not counted.

Each target is looked up as in Record selection. The outcomes, and the rule behind each:

Outcome Finding Severity RFC section
No SPF record at the target No SPF record at the include or redirect target: permerror. FAIL 5.2, 6.1
Two or more SPF records N SPF records: receivers return permerror. FAIL 4.5
A name already being expanded higher in the same branch Include loop FAIL none (Tamperbox; a loop exhausts 4.6.4)
A name containing % SPF macros need a sender IP and envelope identity CHECK 7
More than ten levels, or more than 100 names Expansion limit reached CHECK none (Tamperbox bound)
SERVFAIL, timeout, truncated or inconclusive answer, or a name the server refuses Lookup incomplete: retry before using this result. CHECK 4.4

For an include, section 5.2 maps a recursive “none” to “return permerror”; for a redirect, section 6.1 says “if no SPF record is found, or if the <target-name> is malformed, the result is a “permerror” rather than “none”.”

A branch error appears as “domain: reason”. Failing findings inside an included record (syntax errors, broken targets) are carried up to the result; CHECK findings inside it are not, and the hygiene rules below run on the top record only. An incomplete branch adds “Incomplete lookup tree” (CHECK) and every count is shown as “at least N”.

These run on the top record. Section 4.7 sets the default: “If none of the mechanisms match and there is no “redirect” modifier, then the check_host() returns a result of “neutral”, just as if “?all” were specified as the last directive.” For ptr, section 5.5: “This mechanism SHOULD NOT be published.”

Finding Severity Rule RFC section
No all mechanism CHECK No all and no redirect 4.7
+all allows every server on the internet FAIL +all or bare all none (Tamperbox; the RFC allows it)
?all makes SPF neutral CHECK ?all 4.7
Ends in ~all, Ends in -all PASS ~all or -all none (Tamperbox)
Mechanisms after all are ignored CHECK A mechanism follows all 5.1
redirect is ignored when all is present CHECK Both present 6.1
ptr is deprecated CHECK Any ptr 5.5
Duplicate X CHECK The same term twice, compared exactly; a leading + is ignored none (Tamperbox)
N characters INFO Longer than one 255-character TXT string 3.3

The plan is a Tamperbox method; RFC 7208 neither requires nor describes flattening, though section 10.1.1 calls a record of ip4 terms the “Best record” for DNS cost. It appears when the total is over 10.

Candidates are the top record’s includes with a pass qualifier. One is flattenable only when its whole subtree holds nothing but pass-qualified ip4 and ip6 terms, pass-qualified nested includes and a non-pass all, every lookup in it succeeded, and it has no failing problems. That condition keeps the result equivalent under section 5.2: an include matches only when the included record returns pass, so its pass ranges are exactly what it contributes, and a -all inside it means “not match”, not fail.

Candidates are taken in order of savings (the include plus everything beneath it) until the total is 10 or lower. No plan is chosen when the top record has failures or the tree is incomplete. Each chosen include is replaced in place by its ranges, so they stay ahead of any - or ~ mechanism that follows. The flattened record is split into 255-character strings. If every candidate flattened still leaves it over 10, the plan says so and suggests a subdomain.

An include that cannot be flattened is labelled “Keep: uses a, mx or exists” whatever the reason: it may also use ptr, redirect or a non-pass term, or have failed to resolve. A flattenable include the plan does not need is labelled “Keep: N lookups”.

Where each part of the work happens: the server only asks public DNS (Cloudflare’s resolver, over DNS over HTTPS) for TXT records and returns the answers. Parsing, the lookup count, the include tree, every finding and the flattening plan are computed in the browser from those answers, by static evaluation: no sender address is tested. A pasted record is parsed in the browser; names under the reserved .example domain are answered from synthetic data in the browser.

Section Requirement What the tool does
3.1 SPF records are TXT records only Queries TXT only
3.2, 4.5 One record per name; more is permerror FAIL, then parses the first record
3.3 Multiple strings concatenated without spaces Joins quoted strings before parsing; the flattened record is split back into 255-character strings
3.4 Answers SHOULD fit in 512 octets (450 for the records) Not checked
3.5 Wildcard records are discouraged Not checked
4.3 NXDOMAIN gives “none”; IDNs MUST be A-labels NXDOMAIN is “No SPF record”; names are converted to A-labels before lookup
4.4 DNS errors give temperror Marks the branch incomplete (CHECK)
4.5 Keep records starting with exactly “v=spf1” Same; v=spf10 is discarded
Section Requirement What the tool does
4.6 Any syntax error is permerror Every error is a FAIL finding; all are listed, not only the first
4.6.1 Eight mechanisms, name=value modifiers, names case-insensitive Implemented; terms split on any whitespace
5.1 all takes no value FAIL on a value
5.2, 5.7 include and exists need a domain-spec FAIL when empty; the domain-spec itself is not validated
5.6 ip4 and ip6 networks and prefix lengths Addresses validated; prefix ranges checked
6 redirect and exp at most once; unknown modifiers ignored Duplicate redirect or exp is FAIL; repeated unknown modifiers are ignored for validity
7.1 Macro and domain-spec grammar Not validated
Section Requirement What the tool does
4.1 One global lookup limit across recursion One total for the tree
4.6.2 Mechanisms left to right; stop at the first match Left to right; no match is simulated, so every term is counted
4.6.4 At most 10 DNS-querying terms Counted statically; FAIL above 10
4.6.4 At most 10 address records per MX or PTR record Not checked (no address queries)
4.6.4 SHOULD limit void lookups to two Counts TXT voids only; FAIL above two
4.7 No match and no redirect gives neutral CHECK “No all mechanism”
5.1, 6.1 Ignore terms after all; ignore redirect when all is present Same order and exclusions
5.2 include of a domain without SPF is permerror FAIL
5.5 ptr SHOULD NOT be published CHECK
6.1 redirect to a domain without SPF is permerror FAIL

The tool reads records; it does not run check_host() for a connection. Not covered:

  • the <ip> and <sender> arguments of section 4.1, so no pass, fail or softfail result is produced for any address
  • the address, MX, PTR and A queries of a, mx, ptr and exists (sections 5.3 to 5.7), and the MX and PTR sub-limits of section 4.6.4
  • macro expansion (section 7): a branch whose name holds % is left unexpanded
  • the explanation lookup of exp (section 6.2)
  • the HELO identity (section 2.3), result handling (section 8) and the Received-SPF header (section 9)
  • the elapsed-time limit of section 4.6.4 (the server stops each lookup after 8 seconds instead)
  • The count covers every path. Receivers stop at the first match, so mail matched early can pass a record the tool scores as over the limit.
  • +all is FAIL. RFC 7208 allows it; it makes SPF meaningless, so the tool treats it as broken.
  • Includes in a record with syntax errors are still expanded and counted. A receiver returns permerror before looking any of them up.
  • Separators. Terms split on any whitespace; section 4.6.1 allows only spaces.
  • Prefix lengths with leading zeros such as /024 are accepted; the ABNF in section 5.6 rejects them.
  • Malformed targets. A domain-spec is not checked against section 7.1. A malformed include or redirect target is refused by the server and shown as an incomplete branch (CHECK), where receivers return permerror.
  • DNS errors are shown as incomplete (CHECK), not as the temperror of section 4.4.
  • Void lookups are counted for TXT queries only, so a record whose a or mx names do not exist may pass the void check here and fail at a receiver.
  • U-labels in a record’s include or redirect target are converted to A-labels when looked up, not reported; section 4.3 says IDNs in a record “MUST be encoded as A-labels”.

RFC 7208 is a Proposed Standard, updated by three documents:

  • RFC 8616, section 4: “All U-labels MUST be converted to A-labels before being used for an SPF validation.” A domain typed with non-ASCII labels is converted to A-labels before it is looked up. Its rule on the %{s} and %{l} macros does not apply, since macros are not expanded.
  • RFC 7372, section 3.2: SMTP enhanced status codes for SPF failures. Not relevant: the tool sends no mail.
  • RFC 8553, section 2.1: registers underscored TXT names. It changes nothing the tool checks.

The findings that mention DMARC follow RFC 9989, which obsoleted RFC 7489 in May 2026. The tool does not evaluate DMARC; it uses these sections to explain consequences:

  • Section 4.4.2: “DMARC relies solely on SPF validation of the MAIL FROM identity”, and only an aligned SPF pass counts.
  • Section 5.3.5: a message fails DMARC when no authenticated identifier aligns. A broken SPF record therefore leaves DKIM as the only path to a DMARC pass.
  • Section 7.1: -all can get mail rejected before DMARC runs, even mail an aligned DKIM signature would pass. This is why the guide suggests ~all while DMARC is being rolled out.

RFC 1035 section 3.3 defines a character-string as one length octet followed by at most 255 characters, and section 3.3.14 defines TXT data as one or more of them. That is the “255-octet maximum length of a character-string” RFC 7208 section 3.3 refers to, and the basis for the INFO finding and the 255-character split of the flattened record. The tool counts characters, which equals octets for the ASCII text section 3.1 requires.

Each finding the checker can emit, grouped by what it judges, with what it means for mail sent as the domain and the RFC section behind it. “none” marks a Tamperbox recommendation rather than an RFC rule.

The budget RFC 7208 section 4.6.4 sets: ten DNS-querying terms and two void lookups per evaluation.

Finding Severity What it means for mail Fix RFC section
N of 10 DNS lookups (11 or more) FAIL Evaluations reaching the eleventh lookup return permerror; under DMARC that mail can fail Remove unused includes, flatten, or move a sender to a subdomain 4.6.4
N of 10 DNS lookups (9 or 10) CHECK The next include breaks the record Make room before adding a service none
N of 10 DNS lookups (8 or fewer) PASS Within the limit 4.6.4
N void lookups FAIL More than two empty answers is a permanent error Remove includes whose target has no record 4.6.4

Problems that make the record itself unusable or ambiguous to a receiver.

Finding Severity What it means for mail Fix RFC section
No record at domain. FAIL Nothing is published at the name, so SPF never passes Publish one record 4.3, 4.5
No SPF record at this name. FAIL SPF never passes; DMARC relies on DKIM alone Publish one record 4.5
An SPF record starts with v=spf1. FAIL The text is not an SPF record Start with v=spf1 4.5
N SPF records: receivers return permerror. FAIL Every message fails SPF Merge into one record 4.5
Unknown term X FAIL Permanent error for every receiver Correct the term 4.6.1
Invalid IPv4 address in X, Invalid IPv6 address in X FAIL Permanent error Correct or remove the term 5.6
Prefix too long in X, Invalid prefix in X FAIL Permanent error Use a valid prefix 5.6
include needs a domain, exists needs a domain, redirect needs a domain, exp needs a domain FAIL Permanent error Add the domain 5.2, 5.7, 6.1, 6.2
Invalid all mechanism X FAIL all takes no value Remove the value 5.1
Duplicate X modifier FAIL Permanent error for redirect and exp; other modifiers may repeat under the RFC Remove one 6
Unknown modifier X CHECK Receivers ignore it Remove it 6

How the record ends decides what happens to servers it does not list.

Finding Severity What it means for mail Fix RFC section
No all mechanism CHECK Unlisted servers get neutral; SPF never fails Add ~all or -all 4.7
+all allows every server on the internet FAIL Anyone passes SPF for the domain Use ~all or -all none
?all makes SPF neutral CHECK Unlisted servers are neither allowed nor refused Use ~all or -all 4.7
Ends in ~all, Ends in -all PASS Unlisted servers soft-fail or fail none
Mechanisms after all are ignored CHECK Those senders are not allowed Move all to the end 5.1
redirect is ignored when all is present CHECK The redirect never runs Remove one 6.1

Terms that work but spend lookups or bytes for nothing.

Finding Severity What it means for mail Fix RFC section
ptr is deprecated CHECK Slow, unreliable, skipped by some receivers Replace with ip4/ip6 5.5
Duplicate X CHECK The copy costs a lookup Remove one none
N characters INFO Longer than one TXT string Publish as several quoted strings 3.3

Findings about the records the tool follows, prefixed with the name they were found at.

Finding Severity What it means for mail Fix RFC section
Incomplete lookup tree CHECK Counts are a lower bound Retry before changing DNS 4.4
domain: No SPF record at the include or redirect target: permerror. FAIL The include breaks the record Remove the include 5.2, 6.1
domain: N SPF records: receivers return permerror. FAIL The include breaks the record Ask the provider to merge them 4.5
domain: Include loop FAIL The include refers back to a record above it Break the loop none
domain: Expansion limit reached CHECK Too deep or too many names to inspect Simplify the record none
domain: SPF macros need a sender IP and envelope identity CHECK The branch is not expanded Nothing; evaluate with a real sender 7
domain: Lookup incomplete: retry before using this result. CHECK The branch was not read Retry 4.4
  1. Open the include tree and find the branches that cost the most; the lookup budget numbers them in order.
  2. Remove the includes of services that no longer send for you. This alone fixes most records.
  3. If the record is still over 10, open the flattening plan. Heal it loads the flattened record; copy it from the Flattened record box.
  4. Publish the new record as the only v=spf1 TXT record at the domain, then check the domain again. The count should be 10 or lower.
  5. Put a reminder in the calendar: flattened ranges are a snapshot. Check the flattened includes on their own page every few months, or after the provider announces a change.

A sender on a subdomain with its own SPF record never counts against the main domain; that is the lasting fix for a bulk mailer.

End the record in ~all while DMARC is being rolled out and in -all once every sender is listed. Heal it changes +all to ~all and adds ~all to a record that has none; publish the result.

One unknown mechanism or one bad address fails the whole record for every receiver. Heal it corrects common misspellings of include and removes an invalid ip4 or ip6 mechanism while keeping the valid ones; check the result and publish it.

Replace ptr with the ip4 and ip6 ranges of the servers it covered. Heal it removes ptr; add the ranges before publishing.

Heal it merges them: v=spf1 once, every term from both, and -all if either record had it, ~all otherwise. Publish the merged record and delete the others.

Open the include on its own (Take it further) to see what it publishes. If the service no longer sends for you, remove the include; if it does, ask the provider to fix its record.

Each case is built from the record you entered or looked up, so the finding looks the way it would for you.

Case What it does to your record Heal it offers
Two SPF records Adds a second v=spf1 record Merge into one record
+all Replaces the all mechanism with +all Change +all to ~all
No all at the end Removes the all mechanism Add ~all at the end
Invalid address Inserts ip4:198.51.100.300 Remove invalid address mechanism
ptr mechanism Inserts ptr Remove ptr
Duplicate include Repeats the first include Remove 1 duplicate
Typo in a mechanism Misspells the first include as inlcude: Fix inlcude to include

The page opens on a live check of tamperbox.dev. Its published records and findings can change over time.

For the deterministic walkthrough below, enter harbor-supply.example. This reserved .example domain is answered from synthetic data in your browser; it sends no live lookup. Its record includes four mail services:

v=spf1 include:_spf.inboxcloud.example include:send.bulkpost.example include:mail.ticketdesk.example include:spf.officesuite.example ip4:198.51.100.24 ~all

The expansion costs 11 lookups: the first include nests three more, the second one more, the third uses a and mx, the fourth nests one. SPF lookup budget exceeded: 11 of 10 DNS lookups. The plan flattens _spf.inboxcloud.example (saves 4, 6 ranges) and keeps the rest: mail.ticketdesk.example because it uses a and mx, the other two because flattening them is not needed. The flattened record costs 7 lookups and fits in one TXT string. Heal it loads it; replace the sample with your own domain and every section, card and link is rewritten for it.

The Tamperbox MCP server runs the same checks. Search its catalog for the SPF operation, inspect the input schema, and call it with the domain or the record. Keep the warning block with any conclusion:

  • The count is a static expansion, not an evaluation for a sender address; mail matched by an earlier term can pass a record that is over the limit.
  • a, mx and exists targets are not resolved; macros are not expanded.
  • Flattened ranges are a snapshot of what a provider publishes today.

See Connect an MCP client and Code mode for the calling pattern, and Security and privacy for where the domain travels.

  • A bounded static expansion: ten levels, 100 names, no sender address, no macros, no a, mx or exists resolution.
  • Void lookups are counted only for the TXT queries the tool makes.
  • DNS errors and malformed target names are reported as an incomplete tree, not as the temperror or permerror a receiver would return; see Standards and conformance.
  • Duplicate detection compares whole terms; an explicit + is treated as the default qualifier.
  • One DNS answer, from one public resolver, at one moment.
  • Nothing is stored.