Most SPF articles teach you include: and stop, because that's all most people need to copy-paste a Google Workspace or Microsoft 365 setup and move on. But when a domain starts throwing permerrors, or a security review asks why the record ends in ?all, "copy-paste and hope" runs out. This is SPF record syntax explained the way you'd actually need it during an incident: mechanism by mechanism, qualifier by qualifier, with the evaluation logic a receiving mail server runs at each step.
What an SPF Record Actually Is (and Isn't)
An SPF record is a single line of text published in DNS. It is not a separate record type, it's a TXT record, and it does one job: it lists which servers are allowed to send mail claiming to be from your domain. Nothing more. It doesn't encrypt anything, doesn't sign anything, and doesn't touch message content.
Every valid record starts with v=spf1. That version tag isn't decorative. A receiving server that doesn't see it at the start of the string won't treat the TXT record as SPF at all, even if the rest of the syntax is perfect.
Crucially, SPF checks the envelope sender, the MAIL FROM address used in the SMTP transaction, not the From: header a human reads in their inbox client. This distinction trips up more people than any mechanism syntax does. A message can pass SPF cleanly while displaying a completely different, spoofed-looking From address. That gap is exactly why SPF is only one leg of the authentication stool; understanding how SPF fits alongside DKIM and DMARC matters once you've got the syntax down, because SPF alone can't stop From-header spoofing.
Where the SPF record lives: the TXT entry
A live SPF record, as returned by a DNS query, looks like this:
example.com. 3600 IN TXT "v=spf1 ip4:203.0.113.0/24 include:_spf.google.com ~all"
That's the entire syntax surface: a version tag, a sequence of mechanisms (each optionally prefixed by a qualifier), and sometimes a modifier. Everything that follows in this article is about how to fill in, and correctly order, that sequence. SPF is evaluated left to right with first-match-wins logic, so syntax precision isn't a style preference. A misplaced mechanism or a missing qualifier changes what mail gets authenticated.
SPF Record Mechanisms: The Complete Reference
SPF defines eight mechanisms. Each one answers the same question, does this sending IP match? using a different data source. Understanding what each one actually queries is the foundation of SPF validation logic.
ip4 and ip6: authorizing IP ranges directly
ip4 and ip6 are the simplest and most reliable mechanisms because they require no additional DNS lookup at evaluation time. The IP or CIDR range is embedded directly in the record.
v=spf1 ip4:203.0.113.10 ip4:198.51.100.0/24 ip6:2001:db8::/32 -all
If the connecting server's IP matches any listed address or range, the mechanism matches. These are the mechanisms to prefer whenever you control the sending infrastructure directly, since they don't consume any of the lookup budget covered later in this piece.
a and mx: authorizing based on existing DNS records
The a mechanism authorizes any IP address that the domain's own A/AAAA record resolves to. The mx mechanism authorizes any IP listed in the domain's MX records, useful when your mail servers also send outbound mail.
v=spf1 a mx -all
v=spf1 a:mail.example.com mx:example.org -all
Both can take an optional domain argument (as shown with mail.example.com and example.org) and an optional CIDR suffix. Both also trigger a DNS lookup, which matters once you start counting toward the 10-lookup ceiling.
ptr and exists: the rarely-used and discouraged mechanisms
ptr checks whether the reverse DNS (PTR) name of the connecting IP resolves back to the domain, forward-confirmed by an A lookup. It's expensive, potentially several lookups for a single mechanism, and slow, which is why the SPF specification itself discourages it and most modern guidance tells you to avoid it entirely.
v=spf1 ptr -all
exists is a niche macro-driven mechanism. It performs an A lookup on a domain name you construct (often using SPF macros like %{i} for the sending IP), and matches if that lookup returns any address at all. It's used almost exclusively for complex, per-recipient or per-sender authorization schemes, and it's rare outside large-scale mail platforms.
v=spf1 exists:%{i}.spf.example.com -all
include: delegating authorization to another domain's SPF record
include is the mechanism nearly every tutorial covers, because it's what you paste when a third-party sender (Google Workspace, a marketing platform, a helpdesk tool) tells you to authorize their infrastructure.
v=spf1 include:_spf.google.com -all
include evaluates the target domain's entire SPF record as a nested check. If that nested record produces a match, the include mechanism matches. If it doesn't, evaluation falls through to the next mechanism in your record, it does not automatically fail your whole record.
A record like v=spf1 ip4:203.0.113.0/24 include:_spf.google.com ~all shows three mechanism types working together: an IP range, a delegated include, and a softfail catch-all. That's the shape most production SPF records actually take. A direct IP allowance for infrastructure you own, an include for a third-party service, and a qualifier-controlled catch-all at the end.
The all Mechanism and Qualifiers: -all, ~all?all, +all
all always matches. It's typically placed last, as the catch-all for anything not matched by earlier mechanisms. But all by itself doesn't tell a receiver what to do. That's the job of qualifiers.
A qualifier is a single-character prefix you can put on any mechanism, not just all: + (pass, the default if omitted), - (fail), ~ (softfail), and ? (neutral). So -ip4:203.0.113.10 is valid syntax meaning "explicitly fail if it's this IP", qualifiers aren't exclusive to the catch-all, they're a general-purpose prefix.
SPF hardfail -all vs SPF softfail ~all: what receivers do differently
SPF hardfail -all tells receiving servers: if nothing else matched, treat this as an unauthorized sender and reject or bounce it. SPF softfail ~all tells them: nothing else matched, treat it with suspicion (commonly accepted but flagged, or routed to spam) without an outright rejection.
The practical difference shows up in how these interact with DMARC. A domain running -all gives DMARC a clean, unambiguous SPF fail signal to enforce a p=reject policy against. A domain running ~all gives receivers more discretion, which is why many organizations start with ~all during rollout and tighten to -all once they've confirmed all legitimate senders are accounted for.
?all (neutral) explicitly says "no policy assertion", functionally close to having no SPF opinion at all on unmatched mail. It's rarely the right end-state for a production domain.
Why +all is a broken record, not a permissive one
+all looks like the "permissive" option, but it's effectively broken. It tells every receiving server that any IP on the internet is authorized to send as your domain, because + is the pass qualifier and all matches everything. A record ending in +all provides zero protection and is a known misconfiguration, not a legitimate lenient setting. If you see it in an inherited record, treat it as a bug to fix, not a policy to preserve.
Modifiers: redirect= and exp=
Mechanisms and modifiers look similar in a raw TXT string, but they're evaluated differently. Modifiers use a name=value format, and rather than being checked in the left-to-right sequence like mechanisms, they apply globally to the whole record's evaluation.
There are two standard modifiers: redirect= and exp=.
SPF redirect modifier vs include: when the record delegates entirely
The SPF redirect modifier points evaluation to a completely different domain's SPF record, as a full substitute for the rest of the current record.
v=spf1 redirect=_spf.example-provider.com
This is the key distinction from include: include nests a check and falls through on no-match, letting you stack multiple mechanisms and includes in one record. redirect= replaces the entire evaluation outright. There's no falling through to a local all mechanism after it, because there typically isn't one. redirect= is most often used when a domain wants to centrally manage its entire SPF policy from one shared record referenced across many subdomains or brands, rather than authorizing one additional sender alongside others.
exp= is far less common. It points to a TXT record containing a custom explanation string that a rejecting server can return in its SMTP response, but adoption is spotty since many mail servers don't bother displaying it. Most records you'll encounter in practice skip it entirely.
SPF Validation Logic: Evaluation Order and the DNS Lookup Limit
Once you understand each mechanism and modifier, the remaining question is how a verifying mail transfer agent actually applies them, because SPF validation logic is stricter and more mechanical than it first appears.
How a verifying MTA walks the record top to bottom
The evaluating server reads mechanisms strictly left to right. As soon as one mechanism matches, its qualifier's result (pass, fail, softfail, or neutral) is returned immediately, and no later mechanisms are checked. That means mechanism order matters. Putting a narrow ip4 allowance after a broad include that already matches the same IP wastes the specific mechanism entirely, since the include already resolved the check.
SPF DNS lookup limit: what counts toward the 10-lookup cap
This is where most real-world SPF failures actually originate, and it's the part most "just add an include" tutorials skip. The SPF specification, RFC 7208, caps the number of DNS-lookup-triggering mechanisms at 10 per evaluation. The mechanisms that count against that ceiling are include, a, mx, ptr, exists, and the redirect= modifier. Each include chases the nested record's own lookups too, so a single include line can silently cost several of your 10.
ip4, ip6, and all do not trigger DNS lookups and never count toward the limit. They're evaluated purely against data already present in the record string.
SPF permerror too many lookups: causes and fixes
Exceeding the 10-lookup ceiling produces an SPF permerror for too many lookups, and the specification requires evaluators to treat this as a permanent error regardless of whether the sender is actually legitimate. A permerror isn't a soft warning by specification, but enforcement in practice is not uniform across receivers. Large mailbox providers, Gmail, Yahoo, and Microsoft among them, tend to handle an over-limit record with more tolerance, often through extended caching or more forgiving fallback behavior, than the bare RFC text implies. Smaller receiving infrastructure, a corporate Postfix or Exim server running a strict SPF implementation, is far more likely to enforce the limit exactly as written and treat the permerror functionally as a fail. The practical consequence is that a bloated record can "work fine" against the mailbox providers you test against most often while quietly failing at exactly the destinations you're least likely to notice.
Practitioners who inherit legacy SPF records often find this the hard way: nested includes from marketing platforms, helpdesk tools, and CRM integrations, added one at a time over years, have quietly pushed the domain past the 10-lookup ceiling long before anyone touched the record directly. The fix is usually consolidation: flattening rarely-changing includes into direct ip4/ip6 ranges, removing unused third-party includes, and confirming the remaining chain actually stays under the cap.
How to Check an SPF Record for Syntax and Logic Errors
Treat an SPF record check as an ongoing diagnostic habit, not a one-time setup task, because third-party services get added to your record faster than anyone remembers to audit it.
A practical workflow:
- Query the live record. Run
dig TXT example.com(or equivalent) and confirm exactly one TXT record starts withv=spf1. Multiple SPF records for the same domain is itself a permerror condition. Publish only one. - Count nested lookups. Walk every
include,a,mx,ptr,exists, andredirect=in the chain, including inside included domains' own records, and total them against the 10-lookup cap. - Confirm a single terminating mechanism. The record should end cleanly with
all(qualified appropriately) or aredirect=, not both, and not neither. - Verify the qualifier matches intent. Confirm
-allis actually appropriate for your DMARC enforcement stage, rather than a leftover~allfrom initial rollout, or worse, a stray+all. - Cross-check against DMARC aggregate reports to see which sources are actually sending and whether they're passing.
This kind of check is easiest to internalize once you've seen it applied to a real message. Email Decoded's header-analysis chapters are built from dissected, real-world header specimens rather than abstracted examples, which is the same approach used here to trace SPF evaluation step by step. If you want to see the pass/fail result of everything covered above show up in a live message, tracing SPF results in the Authentication-Results header is the natural next stop, and it pairs directly with reading email headers to diagnose delivery failures when SPF alone doesn't explain a bounce.
SPF syntax is deliberately compact, which is exactly why sloppy syntax causes outsized damage: one wrong qualifier or one uncounted include chain away from either an open catch-all or a permerror. Once the mechanisms above are second nature, the logical next step is seeing how SPF's pass/fail result feeds into DKIM signature verification failures and DMARC alignment, and eventually into forensic analysis of the full delivery path when a message needs to be traced end to end rather than just checked mechanism by mechanism.