dotvitals

SPF record syntax explained

Updated ·8 min read

SPF, the Sender Policy Framework, is defined by RFC 7208. It is a single DNS TXT record published at the root of a domain that lists which mail servers are authorized to send email on that domain's behalf. When a receiving server gets a message, it looks up the domain in the message's return path, fetches this record, and works through it left to right until a mechanism matches the sending server's IP address.

The record is short, but its syntax carries real consequences: mechanisms, qualifiers, and a hard limit on how many DNS lookups the whole thing may trigger. This guide walks through every part of an SPF record used in practice, using mail.example and relay.example as placeholder hostnames throughout, so the syntax can be checked against a real record without guessing at what each piece does.

Check yours now

The version tag and overall shape

Every SPF record begins with v=spf1, a literal string that identifies the record as SPF version 1 and nothing else. A TXT record on the domain that does not start with exactly this string is not treated as an SPF record at all, even if the rest of its content looks similar, which is why a record must start with v=spf1 and a following space, not a variant like v=spf1.0 or SPF1.

After the version tag, the record is a space-separated sequence of mechanisms, each optionally preceded by a qualifier, ending in exactly one all mechanism. A minimal but complete record authorizing only Google Workspace to send mail for a domain looks like: v=spf1 include:_spf.google.com -all.

Mechanisms that match by network

ip4 and ip6 mechanisms authorize a specific address or CIDR range directly, without a DNS lookup, which makes them the cheapest mechanisms against the lookup limit. ip4:203.0.113.10 authorizes a single address, and ip4:203.0.113.0/24 authorizes an entire block.

a and mx mechanisms authorize the domain's own A/AAAA records or its MX hosts respectively. a means the sending IP must match one of the current A or AAAA records for the domain itself; mx means it must match the IP of one of the domain's own mail servers. Both trigger a DNS lookup and both are commonly written with an explicit domain, such as a:mail.example, to check a different hostname's addresses.

exists:%{i}.spf-check.example is a less common mechanism that performs a DNS A lookup against a constructed hostname, treating any successful answer as a match; it is mostly used for sophisticated rate-limiting or macro-based setups and rarely appears in ordinary records.

include and redirect: pulling in another domain's policy

include:_spf.provider.example evaluates another domain's SPF record as a subroutine: if that record would result in a pass for the connecting IP, the include matches. This is how third-party senders like transactional email platforms or CRM tools are authorized, since their published include gives them the ability to update their own sending infrastructure without every customer having to edit their own record.

redirect=spf.provider.example is different: rather than being one mechanism among several, it replaces the rest of the evaluation entirely with the referenced domain's record. redirect is meant for a record that has nothing of its own to add, and it is a modifier, not a mechanism, so it does not carry a qualifier the way include does.

  • include: pulls in another domain's SPF result but keeps evaluating your own record's later mechanisms if it does not match
  • redirect: hands off the entire evaluation, so nothing after it in your own record is checked

Qualifiers and the terminal all mechanism

Every mechanism can carry a qualifier: + (pass, the default when none is written), - (fail), ~ (softfail), or ? (neutral). include:_spf.google.com and +include:_spf.google.com mean exactly the same thing, since + is implicit.

The record must end in exactly one all mechanism, which matches every remaining IP that no earlier mechanism matched. -all tells receivers to reject mail from anything not explicitly listed; ~all marks unlisted senders as a softfail, which most receivers still deliver but often route to spam; ?all is neutral and provides essentially no protection. A record with no all mechanism, or with content after it, is technically malformed, since anything past a terminal all is unreachable.

A domain still finalizing its sender inventory typically starts with ~all to avoid rejecting legitimate mail from a source it forgot to include, then tightens to -all once confident the list is complete.

The 10-lookup limit and how records commonly exceed it

RFC 7208 caps SPF evaluation at 10 DNS-querying mechanisms: include, a, mx, ptr, exists and redirect all count, each time they are evaluated, even nested inside an include. ip4, ip6 and all do not count, since they need no lookup. Exceeding the limit produces a permerror (permanent error), and most receivers then treat the domain exactly as if it had no SPF record at all, which defeats the purpose of publishing one.

The limit is exceeded most often by stacking several third-party includes, each of which can itself reference further nested includes the domain owner never sees directly. A record with three provider includes can easily cross 10 lookups if even one of those providers' own records references two or three includes of its own.

  • Flatten a provider's include into static ip4/ip6 ranges where the provider publishes them, since static ranges cost nothing against the limit
  • Remove includes for providers that no longer send mail for the domain
  • Consolidate mail through fewer sending platforms where practical, since each platform typically adds its own include

A worked example, read mechanism by mechanism

Consider v=spf1 ip4:203.0.113.0/24 include:_spf.google.com include:mailgun.example a mx -all published for example.test. Reading it left to right: ip4:203.0.113.0/24 authorizes an entire internal server block with no lookup cost. include:_spf.google.com pulls in Google Workspace's own record, counting as one lookup, and evaluates its own nested mechanisms as part of the total. include:mailgun.example does the same for a transactional email provider, another lookup plus whatever it references internally.

a checks the domain's own A/AAAA records, one more lookup, and mx checks its own MX hosts, another. That is already four lookups from the domain's own record before counting anything nested inside the two includes, which is a realistic illustration of how quickly a record with only a few mechanisms approaches the 10-lookup ceiling once every include's own nested cost is added in.

Publishing and verifying a record

The record is a TXT record at the apex of the domain — entered as @, as a blank host, or as the domain itself, depending on what your DNS panel asks for. There must be exactly one; a second v=spf1 record at the same name is a permanent error that removes SPF entirely, and it is what most provider setup wizards will create if you let them add their own rather than merging into yours.

Verify by looking the record up rather than by trusting the panel. Two publishing mistakes are invisible in the form you typed into: the record landing at example.com.example.com because the panel appended the zone to a name that was already qualified, and a long record being stored as multiple TXT strings that do not reassemble the way you intended.

Then run it through the SPF checker, which parses the record, walks every include, and counts the DNS-querying terms against the ten-lookup limit. What it cannot tell you is whether the list of senders is complete — only DMARC aggregate reports can, because they name every source that actually sent mail as your domain. A record that parses cleanly and omits a live sender is a worse outcome than one that does not parse.

Read back what you actually published
dig +short example.com TXT | tr -d '"' | grep spf1

What commonly goes wrong

  • Two SPF records at the apex. Not a warning: receivers return a permanent error and treat the domain as having no SPF at all.
  • Reaching for -all before the sender inventory is confirmed. Start at ~all, confirm from DMARC aggregate reports that every legitimate source passes in alignment, then tighten. Rolling back is one edit and takes effect within the record's TTL, but the mail rejected in the meantime is gone.
  • Publishing ?all, which reads as a cautious middle ground and is not one. Receivers are required to treat a neutral result as identical to having no policy, so it is the same protection as publishing nothing while looking like a decision.
  • Expecting a subdomain to inherit the apex record. SPF is evaluated against the exact domain in the return path, so a subdomain that sends mail needs its own record.
  • Using the ptr mechanism. It costs a lookup, it is slow, and RFC 7208 §5.5 says it SHOULD NOT be used.
  • Copying a very broad range from a support article. A /8 authorises sixteen million addresses; anything from /0 to /7 authorises so much of the internet that it is no better than +all.

Check your domain with the spf checker