The SPF record: syntax, limits, and breakages
Every mechanism, qualifier, and modifier with a copy-paste example, plus the 10-lookup limit that quietly breaks records and our position on ~all versus -all.
An SPF record is one TXT record on your domain listing every server allowed to send mail for it, and it fails in more ways than any other line of DNS we manage. The syntax is small (eight mechanisms, four qualifiers, two modifiers) but the operational traps are real: a hard 10-lookup ceiling, a one-record rule, and a 255-character string limit, each of which can silently turn your record into an error that receivers score against you. This page is the reference we wish had existed when we set up ours.
SPF (Sender Policy Framework, RFC 7208, published 2014) answers a single question for the receiving server: is the IP address delivering this message allowed to send for the domain in its envelope sender? It says nothing about message integrity (that is DKIM's job) and enforces nothing by itself (that is DMARC's). Where it fits in the larger system is covered in our deliverability handbook; this page is the syntax and the failure catalog.
Anatomy of a record
example.com. TXT "v=spf1 ip4:203.0.113.0/24 include:_spf.google.com ~all"
Left to right: the version tag (always v=spf1, always first), then mechanisms evaluated in order, then a catch-all. The first mechanism that matches decides the result and evaluation stops. Order matters: put your highest-volume sources first, and all always last.
The mechanisms, one by one
| Mechanism | Matches when | Costs a DNS lookup |
|---|---|---|
all | Always; terminates the record | No |
ip4: / ip6: | The sending IP is in the listed address or CIDR range | No |
a | The sending IP matches an A/AAAA record of the domain | Yes |
mx | The sending IP matches one of the domain's MX hosts | Yes |
include: | The named domain's own SPF record produces a pass | Yes |
exists: | A constructed hostname resolves at all | Yes |
ptr | Reverse DNS matches the domain | Yes, and do not use it |
ip4: and ip6: are the cheapest and most precise: ip4:203.0.113.7 for one server, ip4:203.0.113.0/24 for a range. Use them when you control the sending IPs. They cost no lookups, which becomes the whole game later.
include: is how you authorize a provider. include:_spf.google.com pulls in Google's record and passes if their evaluation passes. Despite the name, nothing is textually included; it is a nested evaluation, and every nested include inside the provider's record counts against your lookup budget too.
a and mx look convenient and usually are not. mx in particular encodes an assumption that the servers receiving your mail also send it, which stopped being true for most domains once an ESP entered the picture. Your MX records point at whoever receives your mail; your outbound often lives elsewhere. Prefer explicit ip4 or include.
exists: triggers a lookup of a constructed name and passes if it resolves; it enables per-IP macros and almost nobody needs it. ptr is formally discouraged by RFC 7208 itself: slow, unreliable, and some receivers skip it entirely. If you find ptr in your record, remove it today.
Qualifiers: the character before the mechanism
| Qualifier | On match | Typical use |
|---|---|---|
+ | Pass | Default; include: means +include: |
- | Fail | -all: unlisted senders hard-fail |
~ | Softfail | ~all: unlisted senders are suspect |
? | Neutral | Effectively no opinion; rare and mostly useless |
In practice you will only ever type a qualifier on all, which brings us to the argument.
~all or -all: our position
Publish ~all, and pair it with an enforced DMARC policy.
The case: -all asks receivers to refuse mail at SMTP time on SPF evidence alone, and SPF evidence is structurally unreliable, because forwarding rewrites the sending IP. Every message forwarded from your domain (a university alumni address, an old work account someone redirects) fails SPF at its final destination by design. In June 2026 we watched this class of failure from the receiving side, when Gmail rejected 73% of the mail we were forwarding to an archive; the full incident is in our SPF/DKIM/DMARC overview. With ~all, a forwarded message survives SPF to be judged on its DKIM signature. With -all, some receivers never get that far.
Meanwhile -all buys you almost nothing that DMARC does not do better. Spoofing protection is DMARC's job, done with alignment and your published policy, after both SPF and DKIM have been considered. A domain at p=reject with ~all is far better defended than a domain at -all with no DMARC.
The tradeoff, named: ~all alone, without DMARC, deters nobody, since softfail carries no mandatory consequence. If you genuinely will not publish a DMARC record, -all is the lesser evil. Our position assumes you will do the whole system, because half the system is not worth much.
The 10-lookup limit and flattening
RFC 7208 section 4.6.4 caps evaluation at 10 DNS-querying mechanisms per check. include, a, mx, exists, ptr, and the redirect modifier each count, including every nested lookup inside every include chain. Cross the line and the receiver returns permerror, a permanent evaluation error. Under DMARC, permerror means SPF contributes nothing, so if DKIM also fails you are now failing DMARC because your record was too long. (A separate cap: more than 2 lookups returning no answer, so-called void lookups, is also a permerror.)
This ceiling arrives faster than it reads. A plausible small-business record:
"v=spf1 include:_spf.google.com include:sendgrid.net include:servers.mcsv.net include:_spf.salesforce.com ~all"
Four includes, but each expands: Google's chain alone resolves through multiple nested includes. Ten is not a distant limit; it is two or three SaaS signups away for most companies. Check where you stand with an SPF-specific checker (MxToolbox and dmarcian both count lookups for free) rather than eyeballing the record, because the nested expansions are invisible in your own DNS.
When you are over, in order of preference:
- Prune. Every include should map to a tool that still sends for you. Records accrete; vendors churn. Remove before you optimize.
- Move bulk sending to a subdomain. Your newsletter ESP can authenticate on
mail.example.comwith its own SPF record and its own untouched lookup budget. This also separates reputations, which we recommend anyway. - Flatten last. Flattening replaces includes with the literal
ip4:/ip6:ranges they resolve to, which costs zero lookups. The trap: providers change IPs without telling you, and a manually flattened record rots silently until mail starts softfailing. Flatten only with automated re-resolution (several DNS providers and DMARC vendors sell exactly this), never by hand-pasting today's IPs.
The modifiers
redirect= replaces the record wholesale: v=spf1 redirect=_spf.example.net means "evaluate that domain's record as if it were mine." Useful for many domains sharing one policy; it costs a lookup and there is no fallback if the target has no record. exp= attaches an explanation string to failures. Receivers largely ignore it; skip it.
Common breakages, from most to least frequent
- Two SPF records. Merging tools, migrations, and helpful colleagues each add their own TXT. Two records starting with
v=spf1is an automatic permerror, no matter how correct each one is. Merge into one line. This is the first thing to check when SPF mysteriously fails. - Over the lookup limit. Covered above; it is a permerror and it is invisible until a checker counts for you.
- The 255-character split. A single TXT string maxes at 255 characters. Longer records must be split into multiple quoted strings in one record (
"v=spf1 ..." "... ~all"), which resolvers concatenate. Some DNS panels do this for you; some fail loudly; some truncate and fail silently. If your record is long, dig it after saving. +all, or a trailing typo.v=spf1 +allauthorizes the entire internet to send as you. It shows up in real audits more than you would hope, usually as a debugging step that shipped.- The record on the wrong name. SPF is checked on the Return-Path domain. If your ESP sends with the Return-Path
bounce.example.com, the record onexample.comis not consulted for those sends; the subdomain needs its own. (Most ESPs handle this in the DNS they hand you at setup; it is the part people delete because it "looked redundant.") - A dead include. An include pointing at a domain with no SPF record is a permerror. Vendors get acquired, rename, and retire domains; your record inherits the wreckage.
Verify it like a receiver would
dig +short TXT example.com
Confirm exactly one string starts with v=spf1, then run it through a lookup-counting checker, then send a test message to a Gmail account and read Show original: the SPF line names the domain checked and the verdict, which is the ground truth no syntax audit can give you.
Then remember what you built: a list of allowed senders for the envelope domain, nothing more. It breaks on forwarding, it says nothing about the From your reader sees, and receivers treat it as one signal among several. Count your lookups today, keep the record to one line, and spend the time you save on DKIM and DMARC, where the enforcement actually lives.