Quick answer
An SPF record is a single DNS TXT record, published at your domain's name, that lists which servers may send mail using that domain. It begins with v=spf1 and reads left to right: the first mechanism that matches the connecting IP ends evaluation and decides the result, and nothing after it is consulted (RFC 7208 §4.6.2). A typical record looks like this:
example.com. 3600 IN TXT "v=spf1 include:_spf.google.com ip4:198.51.100.10 ~all"
That line authorises the hosts in Google's own SPF record and the address 198.51.100.10, then softfails everything else. The rest of this reference is what every part between v=spf1 and the trailing all actually means.
One record, one TXT string
A domain publishes exactly one v=spf1 record, and it lives in a single TXT string. Two v=spf1 records at the same name do not add up: they are a permerror that stops SPF passing for every message. If you send through more than one platform, you merge those platforms into the one record rather than publishing a second. For how SPF sits alongside DKIM and DMARC, see email authentication explained.
Mechanisms
A mechanism is a test against the connecting IP. SPF defines eight of them (RFC 7208 §5):
all: always matches. It is the catch-all you place last as an explicit default (§5.1).include:<domain>: runs a full SPF check on another domain and matches only if that check returns pass (§5.2). This is how you authorise a mail provider that publishes its own record.aora:<domain>: matches if the connecting IP is one of the domain's A or AAAA addresses (§5.3). With no domain given, it uses the current domain.mxormx:<domain>: matches if the IP is one of the addresses of the domain's MX hosts (§5.4).ip4:<network>andip6:<network>: match if the IP falls inside the given CIDR block (§5.6). With no prefix length,ip4defaults to/32andip6to/128, an exact match.exists:<domain>: macro-expands a name and matches if that name has any A record (§5.7), which lets a record ask a policy question keyed on the sender or IP.ptrorptr:<domain>: reverse-maps the IP, then forward-confirms the resulting names against the domain (§5.5). RFC 7208 says this mechanism SHOULD NOT be published, because it is slow, unreliable under DNS errors, and heavy on the reverse-DNS servers. Support it if you read it, but do not write it.
The four qualifiers
Every mechanism can carry a qualifier that sets the result returned when the mechanism matches (RFC 7208 §4.6.2):
+pass: the connecting IP is authorised.-fail: the IP is not authorised.-allis a hardfail.~softfail: the IP is probably not authorised, but the receiver should accept and mark the message rather than reject it.~allis a softfail.?neutral: the record makes no assertion either way.
The qualifier is optional and defaults to +, so a bare mx means exactly the same as +mx. This is also the cleanest way to state the difference between a mechanism and a qualifier: the mechanism decides whether the connecting IP matches, and the qualifier decides what result a match produces. The qualifier you attach to the final all is the one that matters most, and ~all versus -all has its own page.
Evaluation order: first match wins
Terms are evaluated from left to right. Each mechanism is checked in turn, and the first one that matches ends evaluation and returns its qualifier's result (RFC 7208 §4.6.2). Anything to the right of the first match is never reached, which is why all belongs at the very end: it matches every address, so any mechanism placed after it is dead. If no mechanism matches and no redirect is present, the result is neutral, exactly as if the record had ended in ?all (RFC 7208 §4.7).
The redirect and exp modifiers
Modifiers appear at most once each and their order does not matter (RFC 7208 §6):
redirect=<domain>: if no mechanism has matched, evaluation continues with that domain's SPF record, and its result becomes your result (§6.1). Unlikeinclude, which contributes a single match on pass,redirecthands over the whole outcome, so it is for pointing several of your own domains at one shared record, not for authorising a third party.exp=<domain>: names a domain whose TXT record supplies an explanation string, returned to the sender when the check ends in fail (§6.2). It changes no result; it only annotates a rejection.
Because a redirect runs only when nothing matched, a record that ends in an all mechanism never reaches its redirect. In practice you use one or the other to terminate a record, not both.
The 10 DNS-lookup limit
This is the rule that quietly breaks records that used to work. An evaluator MUST limit the number of DNS-querying terms to 10 during a single SPF check (RFC 7208 §4.6.4). Exceed it and the result is permerror, which means SPF neither passes nor cleanly fails and can silently drop your DMARC alignment on the SPF path.
Only some terms count. The include, a, mx, ptr, and exists mechanisms and the redirect modifier each cost one lookup against the limit of 10. The all, ip4, and ip6 mechanisms require no DNS at all and cost nothing; the exp modifier is also exempt, because its lookup happens after the record has finished evaluating. A nested include is the trap here: every lookup inside the record it pulls in counts too, so one third-party include can consume most of your budget by itself.
Two narrower limits sit underneath the main one (§4.6.4). A single mx term may not resolve more than 10 mail hosts, a single ptr term is capped at 10 address records, and no more than two "void" lookups (queries that return no record or a name-error) are allowed across the whole evaluation. Crossing the 10-term cap is common enough that fixing the SPF too-many-DNS-lookups permerror is a task of its own, and adding a new sending platform is the usual trigger. You can check how many lookups your live record uses before it bites.
Macros
SPF can build DNS names on the fly with macros (RFC 7208 §7). A macro is a percent sign and a letter in braces: %{s} is the sender address, %{l} its local part, %{o} and %{d} the sender and the checked domain, %{i} the client IP, %{h} the HELO or EHLO name, and %{p} the validated reverse-DNS name. The escapes %%, %_, and %- stand for a literal percent, a space, and a URL-encoded space. Macros mostly appear inside an exists mechanism to look up per-sender or per-IP policy; most ordinary records never need one.
Related guides
- Email authentication explained: SPF, DKIM, DMARC, and PTR
- SPF ~all vs -all: what each qualifier tells receivers to do
- SPF too many DNS lookups: fix the PermError before it breaks delivery
- Multiple SPF records on one domain: why it causes permerror
- SPF PermError after adding a new sender: cause and fix
- DKIM selectors and keys: what they are and how to find yours