SPF Record Syntax: Mechanisms, Qualifiers, Modifiers, and Macros
SPF record syntax follows one shape: a single DNS TXT record that starts with v=spf1, followed by space-separated terms — mechanisms with optional qualifiers, then modifiers — evaluated left to right until the first match. Here is a complete record:
v=spf1 ip4:192.0.2.0/24 include:_spf.example.com -all That one line authorizes a /24 network and a third party’s servers, then fails everything else. Every rule governing it lives in RFC 7208, the SPF standard published in April 2014.
This page is the full reference: every mechanism, every qualifier, both modifiers, the complete macro table, evaluation order, DNS lookup limits, and record placement rules — each with the RFC 7208 section that defines it. If you want protocol fundamentals first — why SPF exists and how it fits with DKIM and DMARC — start with our SPF guide. If you build and maintain records, bookmark this.
SPF Record Syntax at a Glance
An SPF record is one string of text in the RDATA of a single TXT record, and its grammar has exactly three kinds of parts: a version tag, mechanisms (each with an optional qualifier), and modifiers. The version tag must be exactly v=spf1 — a record starting v=spf10 is discarded, not partially matched (§4.5).
The grammar in brief, from §3 and §4.6.1:
| Part | Form | Role |
|---|---|---|
| Version | v=spf1, always first, exact | Selects the record (§4.5) |
| Mechanism | [qualifier]name[:arg][/cidr] | Tested against the client IP; can match or not match (§4.6.2) |
| Qualifier | + - ~ ? before mechanism | The result returned when its mechanism matches (§4.6.2) |
| Modifier | name=value, at most once each | Extra information; never matched (§6) |
Terms are separated by spaces. Mechanism names are case-insensitive, and terms containing none of =, :, or / are mechanisms (§4.6.1). One syntax error anywhere invalidates the whole record: check_host() — the receiver’s evaluation routine, as the RFC names it — validates SPF syntax first and returns PermError immediately if anything is malformed, without evaluating a single term (§4.6). That is why one stray character can fail authentication for every message a domain sends.
SPF Mechanisms
Eight mechanisms exist, and each either matches the connecting IP or does not. RFC 7208 §5 splits them into basic framework mechanisms (all, include) and designated-sender mechanisms (a, mx, ptr, ip4, ip6, exists). Before the full table: most real-world records use only include, ip4, ip6, and all — the rest of this reference exists so you can read other people’s records, not because your own needs them.
| Mechanism | Syntax | Matches when… | Counts toward 10-lookup limit? | RFC § |
|---|---|---|---|---|
all | all | Always | No | §5.1 |
include | include:domain | The referenced record returns Pass | Yes | §5.2 |
a | a[:domain][/cidr] | Client IP is among the domain’s A/AAAA addresses | Yes | §5.3 |
mx | mx[:domain][/cidr] | Client IP is an address of one of the domain’s MX hosts | Yes (plus per-MX address caps) | §5.4 |
ptr | ptr[:domain] | Reverse DNS validates into the target domain | Yes | §5.5 |
ip4 | ip4:network[/cidr] | Client IP is inside the IPv4 network | No | §5.6 |
ip6 | ip6:network[/cidr] | Client IP is inside the IPv6 network | No | §5.6 |
exists | exists:domain | The constructed domain has any A record | Yes | §5.7 |
You can parse any published record term-by-term — with each mechanism’s cost and RFC cite attached — using the SPF syntax inspector.
all
all always matches, which is why it belongs at the end as the explicit default (§5.1). Everything after it is dead text: “Mechanisms listed after all MUST be ignored,” and any redirect= modifier is ignored whenever all appears anywhere in the record (§5.1). A record without a trailing all or redirect= silently defaults to Neutral (§4.7).
include
include:domain recursively evaluates the referenced domain’s SPF record and matches only when that evaluation returns Pass (§5.2). It is not a splice. The RFC itself concedes the name was “poorly chosen”: a -all inside an included record does not fail your outer record — a Fail, Softfail, or Neutral inside simply means “no match here, keep going” (§5.2). Better mental model: if-match. One consequence worth knowing: if the included domain publishes no SPF record at all, include returns PermError (§5.2).
a and mx
a matches when the client IP is one of the target domain’s A or AAAA addresses; mx matches when it is an address of one of the domain’s MX hosts (§5.3, §5.4). Both default to the current domain when no argument is given, and both accept dual CIDR suffixes — a/24 compares only the top 24 bits, and a:example.com/24//64 sets IPv4 and IPv6 prefixes separately (§5.3). Note the cost asymmetry: mx is one term against the 10-lookup limit, but evaluating it triggers one MX query plus an address query per MX host, capped at 10 address records before PermError (§4.6.4).
ip4 and ip6
ip4: and ip6: test whether the client IP falls inside a literal network, using a colon — ip4:192.0.2.0/24, never ip4= (§5.6). Omitted CIDR lengths default to /32 and /128, i.e., exact-address match, and truncated addresses like 192.0.2 are not permitted (§5.6). These are the only designated-sender mechanisms with zero DNS cost, which makes them the cheapest terms in any record.
exists
exists:domain constructs a domain name, queries it for an A record, and matches if any A record comes back — regardless of its value, and always an A query even on IPv6 connections (§5.7). One lookup, arbitrary logic. Combined with macros (§7), it powers dynamic authorization: publish per-IP hostnames in a zone you control, and the record authorizes exactly those IPs without ever listing them. This is the pattern behind Salesforce’s current record, shown in the examples below.
ptr (do not use)
RFC 7208’s own heading for §5.5 is literally "ptr" (do not use): the mechanism “SHOULD NOT be published” because it is slow, unreliable under DNS errors, and burdens the .arpa name servers. Receivers must still support it, but you should never publish it.
41,728 domains — 1.4% of SPF-enabled domains — still publish the deprecated ptr mechanism
Next step: paste your own record into the SPF syntax inspector and confirm every mechanism in it earns its DNS cost.
SPF Qualifiers: +, -, ~, ?
A qualifier is a single prefix character that sets the result returned when its mechanism matches — and when you omit it, the default is + (§4.6.2). mx and +mx are the same term.
| Qualifier | Result | Meaning (§2.6) |
|---|---|---|
+ | Pass | The client is authorized (default when omitted) |
- | Fail | The client is explicitly not authorized |
~ | Softfail | Probably not authorized; the domain isn’t willing to state a hard Fail |
? | Neutral | The domain asserts nothing about the client |
In practice the choice comes down to the final term: -all versus ~all. Receivers treat them differently, and mail flows through forwarders complicate the picture — we cover the trade-off in depth in softfail vs hardfail.
If you also enforce DKIM and DMARC, publish -all; if you are still discovering senders, start with ~all and tighten once softfail vs hardfail settles the trade-off for your domain.
SPF Modifiers: redirect and exp
Modifiers are name=value pairs — always an equals sign, never a colon — that provide information rather than being matched (§6). RFC 7208 defines two, each allowed at most once; a duplicated redirect= or exp= is a PermError, while unrecognized modifiers are ignored no matter how often they appear (§6).
redirect=
redirect=domain hands the entire evaluation to another domain’s record — but only after every mechanism has failed to match (§6.1). The other record’s result becomes your result, with one sharpening: where a missing record would normally give None, a redirect= to a domain with no SPF record gives PermError (§6.1). Two rules people miss: redirect= counts as one DNS lookup (§4.6.4), and it “MUST be ignored” whenever an all mechanism appears anywhere in the record (§6.1).
How it differs from include:
| Behavior | include: (§5.2) | redirect= (§6.1) |
|---|---|---|
| Matches on | Pass only; otherwise evaluation continues | Not a mechanism; applies after all misses |
| Effect of referenced result | Only Pass propagates as a match | Result replaces yours entirely |
With all in the record | Unaffected | Ignored |
| Referenced record missing | PermError | PermError |
| Intended use | Crossing administrative boundaries | Sharing one policy across your own domains |
exp=
exp=domain names a TXT record whose macro-expanded string is returned as the explanation when a message Fails (§6.2). It is the only term in the language that never counts toward the lookup limit — §4.6.4 exempts exp explicitly, because its lookup happens after evaluation, and only on Fail.
SPF Macros Reference
Macros are %{...} sequences expanded at evaluation time from properties of the message — and they are the least-covered corner of SPF record syntax despite being fully specified in RFC 7208 §7. The ABNF defines exactly eleven macro letters: s, l, o, d, i, p, h, c, r, t, v (§7.1).
| Macro | Expands to (§7.2) | Notes |
|---|---|---|
%{s} | The full sender, e.g. [email protected] | |
%{l} | Local-part of the sender (user) | Limits caching (§7.3) |
%{o} | Domain of the sender | |
%{d} | The domain being evaluated | |
%{i} | The connecting client IP | Dotted-quad IPv4 / dot-separated nibbles IPv6 (§7.3) |
%{p} | Validated reverse-DNS name of the IP | ”do not use” (§7.2, §5.5) |
%{v} | in-addr for IPv4, ip6 for IPv6 | |
%{h} | HELO/EHLO domain | |
%{c} | Client IP, human-readable | exp text only (§7.2) |
%{r} | Domain of the host performing the check | exp text only (§7.2) |
%{t} | Current timestamp (seconds since epoch) | exp text only (§7.2) |
Three literal escapes complete the set: %% is a percent sign, %_ a space, %- a URL-encoded space (§7.1).
Transformers modify the expansion inside the braces (§7.3). A digit keeps only that many right-hand parts: %{d2} on email.example.com gives example.com. The letter r reverses the parts on dots: with client IP 192.0.2.1, %{i} is 192.0.2.1 and %{ir} is 1.2.0.192. Custom delimiters re-split on other characters — %{l-} splits the local-part on hyphens — and parts are always rejoined with dots.
The RFC’s own worked example in §7.4 ties it together. With client IP 192.0.2.3 and domain email.example.com:
%{ir}.%{v}._spf.%{d2}
→ 3.2.0.192.in-addr._spf.example.com Pair that with exists: and you get per-IP authorization in a single lookup — the §5.7 example v=spf1 exists:%{ir}.%{l1r+-}._spf.%{d} -all makes decisions “at the level of the user and client IP address.” So what does %{i} mean in an SPF record? It is the connecting client’s IP address — dotted-quad for IPv4, dot-separated nibble format for IPv6 — and it is the macro that makes dynamic gateway records work (§7.2). You can expand any macro against a live IP with the SPF macro debugger.
One caution from the RFC itself: records using s, l, o, or h macros defeat result caching, so §7.3 advises sending domains to avoid them with mechanism directives. Before publishing any macro, expand it against a real client IP in the SPF macro debugger and check the constructed name resolves the way you expect.
How a Receiver Evaluates Your Record
Evaluation is first-match-wins: mechanisms are considered left to right, the first one that matches ends processing, and its qualifier becomes the result (§4.6.2). Order is not stylistic — it is the semantics.
Trace client IP 198.51.100.7 through this record:
v=spf1 ip4:192.0.2.0/24 -a mx ~all
1. ip4:192.0.2.0/24 → 198.51.100.7 not in range → no match, continue
2. -a → IP not among the domain's A records → no match, continue
3. mx → IP matches an MX host's address → MATCH — stop here
Result: Pass (the matching mechanism's implicit "+") The ~all is never reached. Now invert the case: if the IP had matched step 2, the result would be Fail — and no later mechanism could rescue it. A - qualifier early in the record is final for any IP it matches, no matter what an include further right would have said. This also answers the ordering question directly: yes, mechanism order matters, because evaluation stops at the first match (§4.6.2).
If nothing matches and there is no redirect=, the default result is Neutral, “just as if ?all were specified as the last directive” (§4.7). The RFC recommends terminating explicitly with all or redirect= anyway — implicit defaults make debugging harder.
DNS Lookup Limits: What Counts and What Doesn’t
The rule, verbatim from §4.6.4: “SPF implementations MUST limit the total number of those terms to 10 during SPF evaluation … If this limit is exceeded, the implementation MUST return ‘permerror’.” Which terms count is fully enumerated:
| Term | Counts toward the 10? | Extra caps (§4.6.4) |
|---|---|---|
include | Yes | Nested lookups inside it also count |
a | Yes | — |
mx | Yes | ≤10 address (A/AAAA) queries per MX evaluation, else PermError |
ptr | Yes | ≤10 address queries per PTR evaluation; excess records ignored |
exists | Yes | — |
redirect | Yes | — |
ip4, ip6, all | No | Never query DNS |
exp | No | Looked up later, only on Fail — explicitly exempt |
There is a second, separate cap most references skip: void lookups. Queries that return no answer (RCODE 0 with zero answers, or NXDOMAIN) SHOULD be limited to two, with two as the RECOMMENDED default — and “exceeding the limit produces a ‘permerror’ result” (§4.6.4). A record full of stale includes can PermError on void lookups long before it reaches ten total.
148,655 of 3,077,219 SPF-enabled domains — 4.8% — exceed the 10-lookup limit and risk PermError on every message they send
If your own record is over the limit, the step-by-step reduction path lives in fix SPF too many DNS lookups.
Record Placement Rules: One Record, TXT Only, 255-Byte Strings
Exactly one SPF record per name
A domain name “MUST NOT have multiple records that would cause an authorization check to select more than one record” (§3.2) — and the consequence is explicit, not vague: when record selection finds more than one v=spf1 record, check_host() “produces the ‘permerror’ result” (§4.5). Two SPF records don’t merge, don’t race, and don’t half-work. They fail authentication outright. If you have two, merge the mechanisms into one record.
TXT type only
“SPF records MUST be published as a DNS TXT (type 16) Resource Record (RR) only” (§3.1). The dedicated SPF record type (type 99) from SPF’s experimental phase was dropped by the SPFbis working group, which concluded the dual-record-type model “was fundamentally flawed” (§3.1).
107,646 domains still publish the deprecated type-99 SPF record
The 255-byte limit is per string, not per record
This is the rule the SERP most consistently gets wrong. The 255-octet maximum applies to a single character-string inside a TXT record — not to the record. A TXT record can hold multiple quoted strings, and §3.3 is unambiguous: “If a published record contains multiple character-strings, then the record MUST be treated as if those strings are concatenated together without adding spaces.” So this:
example.com. IN TXT "v=spf1 ip4:192.0.2.0/24 include:_spf.exam" "ple.com -all" evaluates as v=spf1 ip4:192.0.2.0/24 include:_spf.example.com -all — note the split lands mid-token and concatenation heals it, because no space is inserted. That is the correct way to publish a record longer than 255 characters. The practical ceiling is elsewhere: §3.4 recommends keeping the whole DNS answer small enough to fit a 512-octet UDP packet (roughly 450 octets of name-plus-records), since oversized answers can be silently dropped by middleboxes that mishandle DNS over TCP.
SPF Record Examples
Four annotated records, from minimal to macro-based. Lookup counts were computed by walking each chain with dig +short TXT on July 28, 2026 — provider records change (Google flattened _spf.google.com from 4 lookups to 1 in December 2025), so always re-verify rather than trust a blog’s cached number.
1. Single provider (Google’s own published string — Set up SPF):
v=spf1 include:_spf.google.com ~all Reads as: authorize Google’s servers; softfail everything else. Cost: 1 lookup — _spf.google.com now resolves to a flat ip4/ip6 list.
2. Provider plus your own mail server:
v=spf1 ip4:192.0.2.10 mx include:_spf.google.com -all Reads as: authorize one fixed IP, your MX hosts, and Google; fail everything else. Cost: 2 lookups (mx, include — the ip4 is free).
3. Multi-SaaS — Google plus Microsoft 365 (a combination Google’s doc publishes verbatim):
v=spf1 include:_spf.google.com include:spf.protection.outlook.com ~all Reads as: authorize both providers; softfail the rest. Cost: 2 lookups — both includes currently resolve to flat address lists.
4. Macro-based dynamic authorization (Salesforce’s live record for _spf.salesforce.com):
v=spf1 exists:%{i}._spf.mta.salesforce.com -all Reads as: for each connection, ask whether <client-ip>._spf.mta.salesforce.com has an A record; pass if yes, fail otherwise. One exists lookup authorizes an entire fleet without listing a single address — the §5.7 pattern running in production.
Want to assemble your own? The SPF generator builds a record from your providers, and the SPF checker validates the result against every rule on this page.
Common SPF Syntax Mistakes
Genuine syntax errors are rarer than vendor marketing implies — and the honest numbers are worth stating. The largest peer-reviewed study of SPF configuration found that only 2.9% of 7,251,736 SPF-enabled domains had any error at all, and true syntax mistakes were 18.15% of those errors — roughly 0.5% of all SPF-enabled domains (Czybik et al., ACM IMC 2023, arxiv.org/abs/2502.08240). Keep the categories separate: syntax errors, lookup-limit PermErrors, and permissive policy choices are three different problems.
The mistakes that do occur are consistent. Each row below is an observed error class with its share of syntax errors where the IMC 2023 study measured it:
| Mistake | Wrong | Right | Rule |
|---|---|---|---|
ipv4 instead of ip4 (11.0% of syntax errors, IMC 2023) | ipv4:192.0.2.0/24 | ip4:192.0.2.0/24 | §5.6 defines ip4/ip6 |
| Space after the colon (16.6%, IMC 2023) | include: example.com | include:example.com | §4.6.1 ABNF permits none |
= where : belongs (Microsoft-documented common error) | ip4=192.0.2.0 / include=example.com | ip4:192.0.2.0 / include:example.com | Mechanisms take :; only modifiers take = (§4.6.1) |
More than one v=spf1 record (15.3%, IMC 2023) | Two TXT records | One merged record | §3.2 / §4.5 → PermError |
Terms after all | v=spf1 -all include:... | v=spf1 include:... -all | §5.1 — silently ignored |
| SPF concatenated with a verification string (7.0%, IMC 2023) | v=spf1 ...google-site-verification=... | Separate TXT records | §4.5 selection breaks |
The =-for-: confusion is documented by Microsoft among the most common errors its support teams see, alongside stray spaces and trailing periods (Microsoft SPF doc, accessed 2026-07-28).
If your DMARC reports already show PermError and you need the diagnosis path rather than the reference, that flow lives in fix SPF PermError.
FAQ
Can a domain have two SPF records?
No. RFC 7208 permits exactly one SPF record per domain name (§3.2), and when record selection finds more than one v=spf1 TXT record, the result is PermError (§4.5) — authentication fails for every message. Merge the mechanisms from both records into a single v=spf1 record instead.
What is the difference between include and redirect in SPF?
include: matches only when the referenced record returns Pass, and evaluation continues if it doesn’t (§5.2). redirect= applies only after every mechanism misses, replaces your record’s result entirely with the referenced domain’s, and is ignored whenever an all mechanism is present (§6.1).
Which SPF mechanisms count toward the 10-lookup limit?
Per RFC 7208 §4.6.4, the terms that count are include, a, mx, ptr, and exists, plus the redirect modifier. The ip4, ip6, and all mechanisms and the exp modifier never query DNS during evaluation and are exempt. A separate cap of two void lookups also applies.
What does %{i} mean in an SPF record?
%{i} is a macro that expands to the connecting client’s IP address — dotted-quad format for IPv4, dot-separated nibble format for IPv6 (§7.2, §7.3). It is most often paired with exists: to build per-IP hostnames, letting one lookup authorize a dynamic set of senders.
Does the order of mechanisms in an SPF record matter?
Yes. Mechanisms are evaluated left to right, the first match stops processing, and that mechanism’s qualifier becomes the result (§4.6.2). An early - qualifier is final for any IP it matches — no later include can override it — and terms after all are never evaluated (§5.1).
How do I publish an SPF record longer than 255 characters?
Split it into multiple quoted strings of at most 255 bytes each, inside one TXT record. Receivers must concatenate the strings without adding spaces (§3.3), so the split can even fall mid-token. Keep the full DNS answer under roughly 450 octets so it fits a single UDP packet (§3.4).
The reference, in four lines
SPF record syntax rewards precision, and the whole standard reduces to a short list worth keeping:
- One record, TXT only. Exactly one
v=spf1TXT record per name; a second one is a PermError, not a merge (§3.1, §3.2). - First match wins. Left to right, stop at the first matching mechanism, return its qualifier (§4.6.2). Order is policy.
- Know your lookup budget. Ten DNS-querying terms, two void lookups — exceed either and every message fails (§4.6.4).
- Macros are standard, not folklore. Eleven letters in §7 power the dynamic records Salesforce runs in production today.
And when you’re ready to see how your record performs against real mail flows, DMARCguard’s aggregate reports show which sources pass, which fail, and exactly what to change.