An SPF record is a single line of DNS that lists who may send email for your domain. The syntax — specified by RFC 7208 — is small but unforgiving: one wrong qualifier or one record too many and it silently fails. This is the field reference.
Anatomy of a record
An SPF record is a TXT record on your domain that always starts with v=spf1
and usually ends with an all mechanism:
v=spf1 include:_spf.google.com ip4:198.51.100.10 -all
Read left to right, the receiver evaluates each mechanism until one matches the sending server, then applies that mechanism’s qualifier.
What SPF actually checks
SPF does not look at the From: address your recipient sees — the one
RFC 5322 defines. It checks the envelope sender, the
MAIL FROM address, also called the Return-Path, and the IP that connected.
This is the source of most SPF confusion. A message can pass SPF perfectly while
the visible From: shows a completely different domain, which is precisely the
gap DMARC closes by additionally requiring
alignment between the two. Chasing an SPF pass
without checking alignment is the most common wasted afternoon in a DMARC
rollout.
Mechanisms
Eight mechanisms exist. In practice you will write four of them.
| Mechanism | Matches | DNS lookups | Example |
|---|---|---|---|
all | Everything — always last | 0 | -all |
ip4 | An IPv4 address or range | 0 | ip4:198.51.100.0/24 |
ip6 | An IPv6 address or range | 0 | ip6:2001:db8::/32 |
a | The domain’s own A/AAAA records | 1 | a or a:mail.yourdomain.com |
mx | The domain’s MX hosts | 1 | mx |
include | Another domain’s SPF (e.g. a provider) | 1 | include:_spf.google.com |
exists | Whether a name resolves, after macro expansion | 1 | exists:%{ir}.spf.yourdomain.com |
ptr | Reverse DNS of the connecting IP | 1 | ptr — deprecated, don’t |
The lookup column is the one to watch: only ip4 and ip6 are free, and the
total across a record must stay at or below ten. That budget is what
counting your lookups is about.
The all mechanism
all always matches, so it must be the last term in the record — anything after
it is never evaluated. Its qualifier is what decides the outcome for every sender
you did not list:
v=spf1 include:_spf.google.com -all
A record without all produces a neutral result for unlisted senders, which is
the same as having no opinion. Always end with one.
The ip4 mechanism
Authorizes a single IPv4 address or a CIDR range. A bare address is treated as
/32:
v=spf1 ip4:198.51.100.10 ip4:203.0.113.0/24 -all
Costs no DNS lookup, which is why replacing an include: with the ranges behind
it (“flattening”) relieves the ten-lookup limit — and why it becomes your problem
when the provider renumbers.
The ip6 mechanism
The same, for IPv6. A bare address is treated as /128:
v=spf1 ip6:2001:db8::1 ip6:2001:db8:1000::/36 -all
If your sending hosts have AAAA records, list them. An ip4: term can never
match a host that connected over IPv6 — so a record that lists only IPv4 ranges
fails for that connection, even though the same host would pass over IPv4.
a, mx and include are unaffected: they resolve whichever address family
the connection used.
The a mechanism
Matches if the connecting IP appears in the A (or AAAA) records of the domain.
Bare a means “this domain”; a:host.yourdomain.com names another:
v=spf1 a a:mail.yourdomain.com -all
A CIDR suffix widens the match to a network around each resolved address —
a:mail.yourdomain.com/24. Costs one lookup either way. Most records carry a
out of habit; if your web server does not send mail, drop it.
The mx mechanism
Matches if the connecting IP is one of the domain’s MX hosts. Convenient when your inbound and outbound mail share hosts, and pointless when they don’t:
v=spf1 mx -all
mx costs one lookup, but it carries a second, separate limit: resolving it
must not require more than ten address lookups. A domain with a dozen MX hosts
therefore permerrors on a single mx term, with a term count of one. This is
the most confusing way an SPF record can fail.
The include mechanism
The workhorse. It evaluates another domain’s SPF record and matches if that
record returns a pass:
v=spf1 include:_spf.google.com include:sendgrid.net -all
Two details cause most include confusion:
- Only
passmatches. If the included record returns fail, softfail or neutral, theincludesimply does not match and evaluation continues to the next term. It does not fail the whole record. - A missing record is fatal. If the included domain has no SPF record at all,
the result is
permerror— the entire record fails, not just that term. This is how cancelling a service silently breaks a domain months later.
Each include costs one lookup, plus every lookup inside the record it fetches.
The exists mechanism
Matches if a hostname resolves to any A record after
macro expansion — %{i} is
the connecting IP, %{ir} its reversed form, %{d} the domain:
v=spf1 exists:%{ir}.spf.yourdomain.com -all
You are unlikely to write this, and likely to meet it: it is how allow-list and reputation services plug into SPF, and how per-sender rules get expressed. If you inherit one, it is deliberate. It costs one lookup.
The ptr mechanism
Matches on the reverse DNS of the connecting IP. RFC 7208 deprecates it in as many words: it is slow, it loads the receiver, and the result depends on reverse DNS you do not control. Some receivers skip it entirely.
Do not add it. If you find it in a record you inherited, removing it is usually safe and always frees a lookup.
Qualifiers
A prefix on a mechanism sets the result when that mechanism matches. + is the
default, so a mechanism with no prefix means pass:
| Qualifier | Result | On all |
|---|---|---|
+ | Pass (default if omitted) | +all — never do this, authorizes the world |
- | Fail (hard) | -all — reject everything else (recommended) |
~ | SoftFail | ~all — mark suspicious but accept (use while testing) |
? | Neutral | ?all — no opinion |
-all vs ~all
This is the decision people agonise over, and under DMARC it matters less than it looks. Both produce an SPF result that fails alignment, and your DMARC policy — not the SPF qualifier — decides what happens to the message:
v=spf1 include:_spf.google.com ~all # while you are still finding senders
v=spf1 include:_spf.google.com -all # once the list is complete
Use ~all while you are still discovering senders in your aggregate reports, and
move to -all once nothing legitimate is missing. Get there — but don’t hold up a
DMARC rollout over it.
+all deserves its own warning: it authorizes every server on the internet to
send as your domain. It appears in records where someone was debugging and never
reverted.
Modifiers
Two modifiers are easy to miss because they use = rather than :, and because
order does not matter for them — a modifier applies to the whole record wherever
it sits.
| Modifier | Meaning |
|---|---|
redirect= | Replace this record with another domain’s entirely |
exp= | Names a TXT record explaining failures |
The redirect= modifier
Hands evaluation entirely to another domain’s SPF record, and returns whatever that record returns:
v=spf1 redirect=_spf.yourdomain.com
Genuinely useful for keeping one shared record across many domains — an agency
pattern, where a single authoritative record covers a client portfolio. Two
catches: it costs a lookup, and it is ignored if an all mechanism is present,
because all always matches first. A record ending -all redirect=… silently
does nothing with the redirect.
The exp= modifier
Names a TXT record whose contents are returned to the sender when the record
produces a fail:
v=spf1 include:_spf.google.com exp=why.yourdomain.com -all
The explanation string is macro-expanded, and only used for fail — never for
softfail or neutral. It is a diagnostic nicety, not a control, and most operators
never set one.
The rules that trip people up
- One record only. A domain must have exactly one SPF
TXTrecord. Two produces apermerror— and a DMARC failure. - Ten DNS-lookup limit. Six terms cost a lookup —
include,a,mx,ptr,existsandredirect— and the total must stay ≤ 10. Ten is allowed; elevenpermerrors. Chained providers blow past this fast. - The
mxmechanism and theptrmechanism have their own sub-limits, which is how a record with a term count of 1 still fails: each is capped at 10 address lookups of its own. Over the cap,mxis fatal whileptrmerely ignores the extras — the difference being that you control your MX records and not the reverse DNS of whoever is connecting. - Two void lookups. Separately from the limit above, no more than two
lookups may return an empty answer. A stale
include:for a service you cancelled canpermerrora record that’s otherwise fine. - 255-character strings. A single TXT string can’t exceed 255 chars; longer records must be split into multiple quoted strings.
ip4:andip6:are free. They cost no lookups, which is what makes flattening work.
Counting your lookups
The count is recursive, and that’s what catches people. include:_spf.google.com
is one lookup, but the record it fetches contains its own include:s, and each
of those counts too. A record that looks like three includes can easily be nine
lookups.
v=spf1 include:_spf.google.com include:sendgrid.net include:servers.mcsv.net -all
Three includes, but the true cost is whatever those three records expand to —
you have to resolve them to know. When a record permerrors and you can’t see
why, over-limit expansion is the usual answer.
When you exceed 10
In rough order of preference:
- Remove senders you no longer use. Most over-limit records are archaeology.
- Replace an
include:with theip4:ranges it resolves to, if the provider publishes stable IPs. This is “flattening” — it costs no lookups, but you now own the maintenance when the provider renumbers. - Move a sender to a subdomain.
news.yourdomain.comgets its own SPF record and its own budget of 10. - Drop
mxandaif they aren’t actually senders. They’re frequently present out of habit.
Flattening is the popular answer and the one that silently breaks a year later. Prefer pruning and subdomains first.
Examples
Google Workspace only:
v=spf1 include:_spf.google.com -all
Microsoft 365 only:
v=spf1 include:spf.protection.outlook.com -all
Google Workspace + SendGrid + a fixed IP:
v=spf1 include:_spf.google.com include:sendgrid.net ip4:198.51.100.10 -all
Still testing (soft fail):
v=spf1 include:_spf.google.com ~all
A subdomain that never sends:
v=spf1 -all
Checking your record
dig +short TXT yourdomain.com
You want exactly one string beginning v=spf1. Two is a permerror regardless
of how correct each one looks individually — and DNS providers make this easy to
do by accident, because adding a second TXT record is a perfectly ordinary
operation.
Common mistakes
+allor a trailing bareall— authorizes any server on earth.- More than one
v=spf1record on the domain. - More than 10 DNS lookups (a hidden
permerror). - A stale
include:that no longer resolves, burning void lookups. - Assuming SPF covers the
From:header — it doesn’t; see above. - Forgetting a real sender — every marketing, CRM, and ticketing platform needs representing, or its mail fails.
Across a portfolio of domains, the lookup count is the thing that quietly drifts out of range as clients add tools — and you are not the one adding them. Watching every domain’s SPF result in the aggregate reports is how you catch it before someone’s invoices stop arriving — that’s the job DMARC Analyzer does, self-hosted, with no per-domain fee, and it is one part of running DMARC for many client domains.
What next
After publishing, confirm alignment in your aggregate
reports — SPF passing isn’t enough
on its own; it must align with your From: domain to satisfy DMARC. If it
isn’t, see why your email is failing DMARC, and
remember SPF is only one leg of SPF, DKIM &
DMARC.