An SPF record is one line of DNS text, and that one line decides whether receiving servers treat your sending IPs as authorized or as impostors. Most senders paste a value from their provider's setup page and never look at it again, which works until the day it does not: a second tool added, a lookup limit crossed, a softfail where a fail was needed. This is the standalone syntax reference, covering every mechanism, qualifier, and modifier with real examples, the 10-lookup limit in depth, worked records for common stacks, and the errors that break authentication silently. We write it as SpamCipher, the cold email platform built for unlimited email sending and automated cold email, and the only platform that can promise you 90%+ inbox placement; a correct SPF record is the first brick in that promise. For where SPF sits alongside DKIM and DMARC, start with the authentication guide; this article is the deep dive on the record itself.

What an SPF record does, and two things it cannot

SPF (Sender Policy Framework) answers exactly one question for a receiving mail server: is the IP address that just connected to me allowed to send mail for this domain? The domain publishes the answer in advance as a TXT record in DNS. When a message arrives, the receiver takes the connecting IP, fetches the SPF record for the domain in play, evaluates the record's terms left to right, and stops at the first term that matches the IP. The qualifier attached to that term becomes the verdict: pass, fail, softfail, or neutral. No cryptography, no message inspection, just an IP allowlist published where anyone can read it.

Two structural limits define what SPF can and cannot protect, and both matter more than most setup guides admit.

Limit one: SPF checks the Return-Path, not the From header. Every email carries two sender identities: the envelope sender (Return-Path, also called MAIL FROM), which is where bounces go, and the From header, which is what the recipient sees. SPF is evaluated against the envelope domain. A spammer can pass SPF with flying colors on a domain they own in the Return-Path while displaying your domain in the From header, and plain SPF says nothing about it. This is why SPF alone stops almost no visible spoofing, and why DMARC exists: DMARC requires the SPF-authenticated domain to align with the From header domain before the pass counts. We return to alignment in the final section, because it changes how you should think about every record you write.

Limit two: SPF breaks on plain forwarding. When a recipient's server forwards your message onward (a university address forwarding to Gmail, an alias relaying to a mailbox), the forwarding server becomes the connecting IP, and that IP is not in your SPF record. The forwarded copy fails SPF through no fault of yours. This is not a bug you can fix in syntax; it is why DKIM, whose signature travels inside the message and survives forwarding, must carry authentication for forwarded mail, and why a DMARC policy should never depend on SPF alone. Keep both limits in mind as you read the syntax below: they explain most of the judgment calls, including the qualifier advice that surprises people.

The anatomy of an SPF record

Here is a realistic SPF record for a company that sends from its own mail server, Google Workspace, and an email service provider:

v=spf1 ip4:203.0.113.25 include:_spf.google.com include:sendgrid.net ~all

Reading left to right, exactly as a receiver evaluates it:

  • v=spf1 is the version tag. It must be the first thing in the record, literally and exactly; a receiver that does not find v=spf1 at the start does not treat the TXT record as SPF at all.
  • ip4:203.0.113.25 is a mechanism: if the connecting IP is 203.0.113.25, this term matches and evaluation stops with a pass.
  • include:_spf.google.com is another mechanism: fetch Google's SPF record and test the connecting IP against it. If Google's record passes the IP, this term matches.
  • include:sendgrid.net does the same for the ESP's sending fleet.
  • ~all is the catchall: any IP that reached this point matched nothing above, and the tilde qualifier says to treat it as a softfail.

That is the whole grammar in miniature: a version tag, then a sequence of mechanisms (tests against the connecting IP), each optionally prefixed by a qualifier (what verdict a match produces), evaluated strictly left to right with first match wins, ending in an all term that catches everything else. There are also two modifiers (redirect= and exp=) that change evaluation rather than testing IPs; they appear less often and we cover them with the qualifiers. Order matters more than people expect: since evaluation stops at the first match, putting your highest-volume source first saves receivers DNS work, and a misplaced all anywhere except the end silently disables every term after it.

SPF record anatomy: version tag, mechanisms that define who may send, and the all catchall
Every SPF record is three parts: the version tag, the mechanisms that authorize senders, and the all term that answers for everyone else.

Every mechanism, with examples

Seven mechanisms exist. Four do the work in nearly all real records; two are situational; one is deprecated and worth knowing only so you can delete it on sight.

ip4 and ip6: authorize addresses directly. The cheapest mechanisms, because they cost the receiver zero DNS lookups. A single address or a CIDR range:

v=spf1 ip4:203.0.113.25 ip4:198.51.100.0/24 ip6:2001:db8::/32 ~all

Use these for infrastructure you control directly: your own SMTP servers, a fixed office egress IP, a VPS that sends transactional mail. Prefer the narrowest range that covers reality; ip4:198.51.100.0/24 authorizes 256 addresses, and every one of them can now send as your domain.

a: authorize whatever your A record points at. The bare form a means "the IPs in this domain's own A/AAAA records"; a:mail.example.com names another host. It costs one DNS lookup and matches if the connecting IP appears in the resolved addresses:

v=spf1 a a:mail.example.com ~all

Useful when a web server also sends mail and you want the record to track the server's IP automatically. The tradeoff is exactly that automatic tracking: whoever controls that A record controls part of your sending authorization.

mx: authorize your inbound mail servers. Matches if the connecting IP belongs to any host in the domain's MX records. It costs one lookup for the MX set plus one per MX host resolved, which adds up quickly. It made sense in the era when the same box sent and received mail; today your MX points at Google or Microsoft, who do not send your outbound mail from their inbound fleet, so mx in a modern record is usually a lookup spent authorizing servers that never send. Include it only if your inbound servers genuinely originate outbound mail.

include: delegate to another domain's record. The workhorse of modern SPF, because almost everyone sends through providers. include:_spf.google.com means "evaluate Google's record against this IP; if Google passes it, this term matches." Two details trip people up. First, the name is misleading: nothing is textually included; it is a nested evaluation, and only a pass in the nested record makes the term match (a fail inside an include does not fail your record; it just means no match, and evaluation continues). Second, every include costs at least one DNS lookup, plus however many the included record spends internally, which is how the 10-lookup limit gets breached without your record visibly changing.

exists: the specialist. exists:example.com matches if the named domain resolves to any A record at all. On its own that is pointless; its power comes from SPF macros, which can splice the connecting IP into the queried name, like exists:%{i}._spf.example.com, letting a DNS server answer per IP dynamically. ESPs and anti-spam services use this internally; hand-written records almost never need it, but you will meet it inside provider includes and should recognize it rather than "simplify" it away.

all: the mandatory catchall. Matches every IP unconditionally, which is why it must be the final term and why its qualifier is the single most consequential character in the record. An all term with a permissive qualifier, or a record missing all entirely (implicit neutral), leaves the door open. The qualifier section next is really the story of what character to put in front of all.

ptr: deprecated, delete on sight. It matched based on reverse DNS of the connecting IP. RFC 7208 says not to use or accept it: it is slow, unreliable, and forces expensive lookups on every receiver. Some receivers ignore it entirely, so it may authorize nothing while still burning lookups. If an inherited record contains ptr, removing it is the rare change that is both a cleanup and an upgrade.

Qualifiers and modifiers

Any mechanism can be prefixed with one of four qualifiers, which set the verdict when that mechanism matches. Unprefixed mechanisms default to +.

  • + (pass): the IP is authorized. +ip4:203.0.113.25 and ip4:203.0.113.25 are identical; nobody writes the plus in practice.
  • - (fail): the IP is explicitly not authorized. As -all, it tells receivers "anything not listed above is forged; reject it."
  • ~ (softfail): "probably not authorized." As ~all, it asks receivers to accept the mail but mark it suspicious. Historically a transition state while you discovered forgotten senders.
  • ? (neutral): "I make no claim about this IP." Functionally close to having no record for that IP; almost never what you want in production.

The perennial argument is ~all versus -all, and the honest answer is less dramatic than the argument. In a pre-DMARC world, the choice mattered enormously: -all was the only way to ask receivers to reject forgeries, at the cost of losing legitimately forwarded mail (which, as covered above, fails SPF by design). In a DMARC world, the DMARC policy, not the SPF qualifier, decides what happens to unauthenticated mail, and major receivers treat ~all and -all nearly identically as inputs to that decision. Our practical position: run ~all while you are still discovering senders and reading DMARC reports, move to -all once your reports show every legitimate source authenticated, and spend the energy you saved on getting DMARC to p=reject, which is the enforcement that actually bites. What you must never publish is +all, which authorizes the entire internet to send as you; we file it under errors below because that is what it is.

Two modifiers round out the grammar. redirect= replaces the record: v=spf1 redirect=_spf.example.com says "evaluate that domain's record as if it were mine." It is the tool for centralizing SPF across many domains you own; note that redirect is ignored if an all term is present, costs a lookup, and, unlike include, the redirected record's verdict (including its fail) becomes your verdict outright. exp= attaches a human-readable explanation to failures: exp=explain._spf.example.com points at a TXT record whose text a rejecting receiver may quote in its bounce. Few receivers display it, and most senders skip it; it exists, it is harmless, and it is the last piece of syntax you will ever need.

The four SPF qualifiers: plus for pass, tilde for softfail, minus for fail, question mark for neutral
Four qualifiers, one decision that matters: which of them sits in front of the final all term.

The 10 DNS lookup limit that breaks SPF records

RFC 7208 caps the DNS lookups an SPF evaluation may perform at ten. Cross the cap and the receiver returns permerror: not a fail, not a softfail, but "this record is broken," which DMARC counts as no SPF authentication at all. It is the most common way a working SPF record dies, and it dies silently, because the record that breaks the limit usually is not the one you edited.

What counts: every include, a, mx, exists, and redirect term costs one lookup, plus everything the fetched record spends internally. (An mx term additionally resolves each MX host, capped separately at 10 hosts.) What does not count: ip4, ip6, all, and the initial fetch of your own TXT record. The counting is recursive, which is the trap: include:_spf.google.com looks like one lookup, but Google's record internally includes three more, so it really costs four. A record with four provider includes can sit at nine lookups, and then one provider restructures internally and pushes you to eleven, breaking your authentication overnight with no change on your side.

Count a realistic record honestly:

v=spf1 include:_spf.google.com include:sendgrid.net include:mailgun.org mx ~all

Google costs 4 (itself plus three nested includes), SendGrid 1, Mailgun typically 2, mx 1: eight lookups, two of headroom, at the mercy of three vendors' internal choices. This is why the practical rules are blunt: audit the true recursive count, not the visible term count; delete mx and a terms that authorize servers that never send; and treat any record above seven true lookups as a maintenance item even though it currently works.

The heavyweight fix is SPF flattening: resolving every include down to its constituent IP ranges and publishing those as raw ip4/ip6 terms, which cost zero lookups. A flattened record cannot breach the limit. The tradeoff is real and permanent: providers rotate their sending ranges without notice, so a flattened record is stale from the day you publish it and needs automated re-resolution (a service or a cron job that re-flattens and republishes). Hand-flattening once and forgetting it trades a visible permerror today for invisible SPF failures next quarter, which is a worse disease than the cure. Flatten if you genuinely need more than ten lookups' worth of providers, and only with automation; otherwise, prune.

One adjacent limit while you are counting: SPF evaluation also caps void lookups (queries returning no answer) at two, so includes pointing at dead domains do not just waste a lookup, they can permerror the record on their own. Stale includes from cancelled vendors are a limit problem as well as a security one.

SPF DNS lookup gauge near the limit of 10, after which records return permerror
Ten DNS lookups is a hard ceiling, counted recursively through every include. At eleven, receivers return permerror and DMARC sees no SPF at all.

Worked SPF records for common stacks

Three records cover most real setups. Each is complete and publishable as the single TXT record at the domain root.

Google Workspace only. All mail, human and outbound, leaves through Google's fleet:

v=spf1 include:_spf.google.com ~all

Four true lookups, six of headroom. This is the record Google's own setup flow asks for, and adding anything else to it should be a deliberate act, not a habit.

Microsoft 365 only. The M365 equivalent:

v=spf1 include:spf.protection.outlook.com -all

Microsoft's include currently spends two lookups internally (three total), leaving ample headroom; Microsoft's own guidance is comfortable recommending -all here because a single-provider record leaves little to discover.

Google Workspace plus an ESP plus a billing tool. The realistic mid-size company: humans on Gmail, campaigns through an ESP, invoices from a billing platform:

v=spf1 include:_spf.google.com include:spf.esp-provider.com include:mail.billingtool.com ~all

Substitute the include hostnames your vendors document (every serious provider publishes theirs; never guess one). Count the recursive total when you add each vendor, keep the highest-volume source first, and re-run the count quarterly, because two of those three includes will change beneath you eventually. When a vendor offers a choice between an include and a dedicated IP to list as ip4, the dedicated IP is cheaper on lookups and more stable, at the cost of updating DNS if you ever migrate.

One structural note that saves audits later: SPF records apply per domain, and subdomains do not inherit them. If you send from mail.example.com, that subdomain needs its own record; the root's record does not cover it. Teams running cold outbound usually send from dedicated subdomains or separate domains precisely so each identity carries its own SPF, DKIM, and reputation, and a syntax mistake on one never bleeds into the primary domain, a separation that matters when a campaign misfires and you are diagnosing deliverability problems under time pressure.

The errors that silently break SPF

Every one of these appears in the wild constantly, and most produce no error message anywhere you would normally look.

  • Two SPF records on one domain. Illegal, full stop: RFC 7208 requires exactly one TXT record starting with v=spf1, and receivers finding two must return permerror, killing both. This happens when a second tool's setup wizard says "add this TXT record" and someone adds it alongside the first instead of merging. The fix is always a merge: one record, all mechanisms, one all at the end.
  • +all, ever. It authorizes every IP on the internet to send as your domain, converting your SPF record into a spammer's welcome mat. It appears in real records as a "temporary" debugging step that stuck, or from a misreading of a setup guide. There is no legitimate production use; if you find it, removing it is urgent.
  • Lookup overflow. Covered in depth above; listed here because it is the breakage you should check first when SPF that "never changed" starts failing. The visible record is identical; a vendor's nested include grew.
  • ptr in inherited records. Deprecated by the RFC, ignored by some receivers, expensive for all of them. Delete it and, if it was authorizing anything real, replace it with the actual ip4 range.
  • Stale includes from cancelled vendors. Every include is a standing grant: whoever controls that domain's record can authorize IPs to send as you. An include for an ESP you left two years ago is both a wasted lookup, a void-lookup risk if the domain lapses, and a supply-chain hole if the domain changes hands. Prune on every vendor exit.
  • Records over 255 characters, unsplit. A single TXT string maxes out at 255 characters. Longer SPF records are legal but must be published as multiple quoted strings within one TXT record ("v=spf1 include:..." " include:... ~all"), which receivers concatenate. Some DNS panels split automatically; others reject or truncate the record. If your record is approaching this length, that is itself a sign it needs pruning or flattening rather than splitting.
  • Typos that demote the record to noise. v=spf 1, spf1 without the v=, a comma between mechanisms, a stray semicolon: any of these and receivers simply do not recognize the TXT record as SPF, so your domain has no policy at all and nothing tells you so. Syntax checkers exist because this class of error is invisible to eyeballs.

How to test an SPF record

Two checks, one from the outside and one from the inside, and together they take five minutes.

From the outside: read what DNS actually serves. Not what your DNS panel displays, what resolvers return:

dig txt example.com +short

Confirm exactly one string starts with v=spf1, that it is the record you think you published (propagation lags and stale panel edits both surprise people), and that split strings concatenate into the record you intended. Then run the recursive lookup count with any SPF validator rather than by hand; the validator follows every include so you do not have to.

From the inside: send a real message and read the verdict. Send from your normal sending path to a mailbox you control at a major provider, open the raw headers, and find the Authentication-Results line:

Authentication-Results: mx.google.com;
       spf=pass (google.com: domain of [email protected] designates 203.0.113.25 as permitted sender)

That line is the ground truth: which IP connected, which domain SPF evaluated (note that it is the bounce domain, not the From), and what verdict the receiver reached. spf=softfail means your record does not cover the path this message took; spf=permerror means the record itself is broken, and the lookup count is the first suspect. Our guide to reading email headers walks the whole header stack if the line does not look like this. Test every distinct sending path you have, not just one: the webmail path, the ESP path, the billing tool. Each may exit through different IPs, and each is a separate opportunity for the record to be wrong. For ongoing coverage rather than a one-time check, continuous DMARC monitoring turns every receiver on the internet into your test harness, reporting the SPF verdict for every source that sends as your domain, including the ones you forgot you had.

SPF's role in DMARC alignment

Return to limit one from the top, because it reframes what your record is for. SPF authenticates the Return-Path domain; recipients see the From domain; DMARC is the layer that connects them, requiring the SPF-passing domain to align with the From domain before the pass protects anyone. In relaxed alignment (the default), the bounce domain bounces.example.com aligns with a From of example.com; in strict alignment they must match exactly. This has a practical consequence for anyone using an ESP: many ESPs use their own bounce domain by default, so your mail passes SPF on the ESP's domain and contributes nothing to your DMARC. The fix is the custom bounce domain (custom Return-Path) option every serious ESP offers, which puts your subdomain in the envelope and makes the SPF pass alignable.

And because forwarding strips SPF (limit two), a message's DMARC survival in the wild rests on DKIM, whose signature rides inside the message. The mature setup treats the two as redundant rails: SPF correct and aligned for the direct path, DKIM signing everything for the forwarded path, DMARC at enforcement watching both. That stack, plus the sending discipline on top of it, is what modern inbox providers reward, up to and including the BIMI logo and blue checkmark that require DMARC enforcement to display. It is also the foundation we insist on before any campaign leaves our own pipeline: SpamCipher, the cold email platform for unlimited email sending and automated cold email, and the only platform that can promise you 90%+ inbox placement, because the promise starts with records that parse, align, and survive contact with the real internet. Write the record once, correctly, count its lookups, and let your DMARC reports referee it forever after.

Know your SPF verdict for every send

SpamCipher monitors your SPF, DKIM, and DMARC across every receiver, catches permerrors and alignment gaps before they cost you placement, and feeds a pipeline built for unlimited, automated cold email at 90%+ measured inbox placement.

Check your authentication

Frequently asked questions

An SPF record is a single TXT record in your domain's DNS that lists which servers are allowed to send email using your domain. When a message arrives, the receiving server checks the connecting IP against that list and produces a verdict: pass if the IP is authorized, fail or softfail if it is not. It is a published allowlist, evaluated left to right, always starting with v=spf1 and ending with an all term that answers for every IP not listed.
If Google Workspace is your only sender, publish exactly: v=spf1 include:_spf.google.com ~all. That single include covers Google's entire sending fleet and costs four of your ten DNS lookups, leaving room for future tools. Add other services only as you actually adopt them, each as an additional include before the ~all, and merge everything into this one record; never create a second TXT record starting with v=spf1, because two SPF records make both invalid.
Start with ~all (softfail) while you are still discovering everything that sends as your domain, then move to -all once DMARC reports confirm every legitimate source passes. In practice the difference is smaller than the arguments about it: once DMARC is deployed, its policy decides what happens to unauthenticated mail, and major receivers treat the two endings similarly as inputs. The ending that is never acceptable is +all, which authorizes the whole internet to send as you.
Ten, counted recursively. Every include, a, mx, exists, and redirect term costs one lookup plus whatever the fetched record spends internally, while ip4, ip6, and all cost nothing. Exceed ten and receivers return permerror, which DMARC treats as no SPF authentication at all. The trap is that provider includes change size without notice: a record at nine lookups can break overnight because a vendor restructured. Audit the true recursive count with a validator, and prune mechanisms that authorize servers that never send.
No. The standard requires exactly one TXT record beginning with v=spf1 per domain, and a receiver that finds two must return permerror, invalidating both. This usually happens when a new tool's setup guide says to add a TXT record and it gets added next to the existing one. The fix is to merge: keep one record, combine all the mechanisms from both, and finish with a single all term. Subdomains are separate: each subdomain that sends mail needs its own record.
Not by itself. SPF validates the envelope sender (Return-Path), while recipients see the From header, and a spoofer can pass SPF on a domain they own while displaying yours in the From. SPF only stops visible spoofing once DMARC requires the SPF-authenticated domain to align with the From domain and sets a policy of quarantine or reject. SPF also breaks on forwarded mail, so DKIM must carry authentication for that path. Treat SPF as one required rail of the SPF plus DKIM plus DMARC stack, not a standalone defense.