SPF Record Syntax Explained: Every Mechanism, Decoded
SPF record syntax looks cryptic because an entire authorization policy is compressed into one DNS TXT string - but the grammar underneath is small: one version tag, a left-to-right list of mechanisms that each either match or don't, an optional qualifier in front of each that says what a match means, and a catch-all at the end for everything that matched nothing. Learn perhaps a dozen tokens and you can read any SPF record on the internet - and more importantly, spot the four or five classic mistakes that silently break them.
Here is the complete grammar, with real records taken apart token by token.
How a receiver reads your record
When mail arrives claiming to be from your domain, the receiving server fetches the TXT record at that domain, confirms it starts with v=spf1, then walks the mechanisms left to right, testing each against the IP address that actually delivered the message. The first mechanism that matches wins - evaluation stops there, and the qualifier on that mechanism becomes the verdict. Nothing after the winning token is even read, which is why order matters and why all only makes sense as the final word.
One structural rule before any syntax: a domain gets exactly one SPF record. Two records beginning v=spf1 do not merge - they produce a permanent error, which receivers treat as no SPF at all. Every new email tool gets merged into the existing record, never added beside it.
SPF record syntax: the mechanisms
| Mechanism | Matches when | DNS lookup? |
|---|---|---|
ip4:192.0.2.0/24 |
Sending IP is in the IPv4 range | No |
ip6:2001:db8::/32 |
Sending IP is in the IPv6 range | No |
a / a:example.com |
Sending IP is an A/AAAA address of the domain | Yes |
mx / mx:example.com |
Sending IP is one of the domain's MX hosts | Yes |
include:_spf.google.com |
The other domain's SPF record passes for this IP | Yes |
exists:%{i}.check.example.com |
A constructed hostname resolves (rare, expert use) | Yes |
ptr |
Reverse DNS matches (deprecated - never use) | Yes |
all |
Always matches - the catch-all terminator | No |
include is the workhorse and the most misread token: it does not include text into your record. It fetches the named domain's SPF record and asks "would this IP pass over there?" - if yes, your include matches; if no, evaluation simply moves on. This is how you authorize a provider whose server fleet changes daily: include:_spf.google.com delegates the fleet inventory to Google.
Two modifiers round out the grammar: redirect=other.example.com hands the whole evaluation to another domain's record (useful for multi-domain organizations; it replaces all, never sits beside it), and exp= points at an explanation string for failures - legal, and almost never worth the bytes.
The four qualifiers
Any mechanism can be prefixed with one of four qualifiers; unprefixed means +:
+Pass - a match authorizes the sender.+ip4:...andip4:...are identical.-Fail - a match positively disauthorizes: "this IP is not us, refuse it." Seen almost exclusively as-all.~SoftFail - "a match is suspicious but don't hard-bounce":~allmarks unlisted senders as likely-not-us while leaving the final call to the receiver's filters.?Neutral - "no opinion", which is also what an absent record yields;?allstates nothing and protects nothing.
The real decision is the last token. -all is the correct destination for a domain that knows its senders. ~all is the sensible transition state while you are still discovering forgotten senders - and with DMARC deployed on top, the practical difference shrinks, because DMARC's verdict dominates. +all authorizes the entire internet to send as you; it is worse than no record and appears in real audits more often than you would hope.
Three real records, dissected
A Google Workspace domain:
v=spf1 include:_spf.google.com ~all
Version tag; one include that matches any Google sending IP; anything else softfails. This is the whole record for thousands of businesses.
A company with Microsoft 365, a sending platform, and an office server:
v=spf1 ip4:203.0.113.25 include:spf.protection.outlook.com include:_spf.sendprovider.example -all
The office server's static IP is checked first - free, no DNS lookup. Then each provider's include. Then hard fail. One record, three senders, merged - the pattern every "add our include to your SPF" onboarding instruction expects to land in.
A misconfigured record, as found in the wild:
v=spf1 a mx include:_spf.google.com include:oldcrm.example ptr +all
Five problems in one line: a and mx burn lookups for servers that often don't send mail; a dead CRM's include still burns a lookup (and a void lookup at that); ptr is deprecated and slow; and +all at the end makes every preceding token decorative - everyone passes anyway.
The limits that break working records
The 10-lookup limit is the one that fails silently as you grow. Every include, a, mx, exists, and redirect costs one DNS lookup - including the nested lookups inside each include. include:_spf.google.com alone resolves through multiple nested records. Cross ten total and the verdict is a permanent error, treated as authentication failure - which means adding a sixth email tool can un-authenticate the first five. Count with any SPF checker; the cures are removing dead includes, using ip4:/ip6: (free) where addresses are stable, and provider consolidation. There is a companion cap of two void lookups - includes pointing at nonexistent records - so dead tools rot your record twice.
The string-length limit: a single TXT string holds 255 characters; longer records must be split into multiple quoted strings inside the same record, which receivers concatenate. Most DNS hosts handle this automatically, but a record rejected or truncated at ~255 characters is this limit surfacing.
The scope rule surprises even experienced admins: SPF is checked against the envelope sender (the Return-Path / MAIL FROM address), not the From line a human reads. A message can display you@yourdomain.com while its envelope sender is your platform's bounce domain - SPF then passes for the platform's domain, not yours. Closing that gap - forcing the authenticated domain to align with the visible one - is DMARC's job, which is why SPF alone stops less spoofing than people assume, and why the pair (plus DKIM, which survives the forwarding that breaks SPF) is the actual standard.
Read any record left to right, count its lookups, check its last token, and confirm there is only one of it - that inspection catches nearly every SPF problem that exists.
Frequently asked questions
What is the difference between ~all and -all in an SPF record?
-all hard-fails any sender not listed in the record; ~all soft-fails them, marking mail suspicious but leaving the decision to the receiver's filters. Use ~all while discovering forgotten senders, -all once the record is known-complete.
How many DNS lookups can an SPF record use?
Ten total - counting every include, a, mx, exists and redirect, plus all lookups nested inside each include. Exceeding ten yields a permanent error treated as authentication failure. ip4 and ip6 mechanisms cost nothing.
Can a domain have two SPF records?
No. Multiple TXT records starting v=spf1 produce a permanent error, treated as no SPF at all. Merge every provider into one record - new tools' includes are added inside the existing string, never as a second record.
Does SPF check the From address the recipient sees?
No - SPF evaluates the envelope sender (Return-Path/MAIL FROM), which platforms often set to their own bounce domain. Aligning the authenticated domain with the visible From line is what DMARC adds on top of SPF.