Inbox
articleMay 21, 20266 min read

SPF Record Syntax Explained for Senders

A field guide to the SPF record: every mechanism, every qualifier, the breakage modes that quietly kill deliverability, and the dig command to verify your work.

By SESMetric Editorial

SPF tells receiving mail servers which hosts are allowed to send mail for your domain. Get the syntax wrong and Gmail, Outlook, and Yahoo treat your transactional mail as forged — even when DKIM and DMARC are in place. This guide walks the full grammar defined in RFC 7208 and the specific mistakes that break SPF in production.

What an SPF record syntax looks like

An SPF record is a single DNS TXT record on the bare domain (or subdomain) that sends mail. It is a space-separated list of terms, beginning with the version tag and ending with a catch-all all mechanism.

A typical record for a domain that sends only through a SaaS provider looks like this:

example.com.  IN  TXT  "v=spf1 include:_spf.sesmetric.com -all"

Reading left to right:

  • v=spf1 — the version tag. Every SPF record must start with this exact string. Anything else is not an SPF record.
  • include:_spf.sesmetric.com — a mechanism. It tells the receiver to look up the SPF record at _spf.sesmetric.com and treat its allowed senders as also allowed for example.com.
  • -all — the closing qualifier-plus-mechanism. all matches every sender; the - qualifier says "fail anything that did not match an earlier mechanism".

That is the whole shape of SPF record syntax. The complexity lives in which mechanisms you choose and which qualifiers you stick on them.

Mechanisms

A mechanism is a rule that either matches the sending IP or it does not. SPF evaluates mechanisms left to right and stops at the first match.

MechanismMatches when…
allalways — used as the terminal default
ip4:192.0.2.0/24the sending IP is in this IPv4 range
ip6:2001:db8::/32the sending IP is in this IPv6 range
a or a:host.examplethe IP matches the A/AAAA record of the named host (or the current domain if no host given)
mx or mx:host.examplethe IP matches an MX host of the named domain
include:_spf.vendor.comthe IP would pass the SPF record of vendor.com
exists:%{i}._spf.example.coma DNS A lookup for the expanded macro returns any record
ptrreverse DNS of the sender resolves to the domain — deprecated, do not use

ip4, ip6, a, and mx are the workhorses for your own infrastructure. include is what you use to delegate to a sending platform. exists is rare and only useful for macro-driven rules. ptr is in the spec but RFC 7208 §5.5 explicitly says receivers should avoid it, and most do.

A worked example

A domain that sends transactional mail through SESMetric, marketing through a different vendor, and one-off mail from a known office IP might publish:

v=spf1 ip4:203.0.113.10 include:_spf.sesmetric.com include:spf.marketing-vendor.example -all

That allows three categories of sender and rejects everything else.

Qualifiers

Every mechanism carries an implicit or explicit qualifier that decides what happens when it matches.

QualifierResult on matchCommon use
+PassThe default. +a and a are identical.
-FailReject hard. Use on the terminal all once you are confident.
~SoftFailAccept but mark suspicious. Use during rollout.
?NeutralNo opinion. Effectively the same as having no SPF for that case.

The terminal all is where qualifiers matter most. -all is a hard fail and is what you want in steady state. ~all is a softer rollout posture — useful while you confirm every legitimate sender is enumerated. ?all is meaningless protection. +all is actively dangerous and is covered below.

How SPF is evaluated

When a receiving server gets a message from you@example.com, it:

  1. Looks up the TXT record for example.com.
  2. Filters for records starting with v=spf1. There must be exactly one.
  3. Walks the mechanisms left to right.
  4. On the first matching mechanism, applies that mechanism's qualifier and stops.
  5. If nothing matches, the result is whatever the terminal all says — Pass, Fail, SoftFail, or Neutral.

Every include, a, mx, exists, and redirect term costs DNS lookups. Counting those lookups correctly is the single biggest source of silent SPF failure.

Common mistakes

These are the breakage modes you will see on real domains, in roughly the order they show up in deliverability tickets.

Multiple SPF TXT records on the same name

A domain may publish only one record beginning with v=spf1. RFC 7208 §3.2 is unambiguous: if a receiver finds two, the result is PermError and SPF is treated as not present at all.

This happens most often when a team adds a new sending vendor by appending a second TXT record instead of editing the existing one. Check with:

dig +short TXT example.com | grep spf1

If you see two lines beginning with "v=spf1, merge them into a single record by combining their include: and ip4: terms.

Hitting the 10-DNS-lookup limit

SPF caps the number of DNS lookups a single evaluation may trigger at 10. Every include, a, mx, exists, ptr, and redirect counts. ip4 and ip6 do not. Nested include: records inherit into your budget — if a vendor's include itself contains four more include: terms, you have already spent five lookups before any of your own.

When you exceed 10, the result is PermError. Receivers treat PermError like no SPF at all, so a record that "looks correct" silently stops authenticating.

To stay under the limit:

  • Audit the chain of every vendor you include:. Tools that flatten SPF can help, but flattening locks you to specific IPs that the vendor may rotate.
  • Replace a and mx mechanisms with explicit ip4: and ip6: ranges where you control the IPs.
  • Drop vendors you no longer use. Old include: entries accumulate.

Publishing +all

+all means "any host on the public internet may send mail as this domain and pass SPF". It exists for testing and should never appear in production. It nullifies SPF entirely and is one of the few SPF misconfigurations that anti-spam vendors will flag on its own.

Always close your record with -all (hard fail) or ~all (soft fail). Use ~all only while you are still discovering legitimate senders; tighten to -all once the inventory is stable.

Missing include: for a SaaS sender

When you add a new transactional email provider, password reset emails start failing SPF the moment you flip the DNS record on your sending domain — unless you also add that provider's include: term. The provider's onboarding docs always specify the exact term to add. Forgetting it is the single most common reason a working domain suddenly produces Fail results after a vendor migration.

If you also send through your own MX hosts, marketing tools, helpdesk software, and CI, every one of those needs to be represented in the SPF record. Authenticating one path while breaking three is worse than not having SPF at all, because DMARC will start rejecting the others.

Verifying your record

Two checks should be part of any SPF change.

Inspect the raw record with dig

dig TXT returns the record exactly as DNS serves it, with no rewriting:

dig TXT example.com +short

Sample output for a clean record:

"v=spf1 include:_spf.sesmetric.com -all"

If the output contains two v=spf1 lines, you have the multiple-record problem. If it is empty, your DNS provider has not propagated the change yet — wait for the TTL to expire and retry.

Walk the includes with an SPF Surveyor pattern

A surveyor expands every include: and redirect recursively, counts lookups, and shows the final flattened set of allowed senders. You can build the same view yourself by repeating dig TXT against every nested name:

dig TXT _spf.sesmetric.com +short
dig TXT spf.marketing-vendor.example +short

Add one lookup for the root record, one for each direct include:, and one for any nested include: inside those. If the total exceeds 10, the record is in PermError territory regardless of what the syntax looks like.

Once dig shows a single clean record, the surveyor shows ten or fewer lookups, and your sending platform's diagnostics show spf=pass, the SPF side of your authentication is done. Pair it with DKIM and DMARC for a complete picture.

Tagsdeliverabilityspfdnsemail-authentication