DMARC Engine
Home/Blog/RFC 7208: the SPF standard explained
Blog

RFC 7208: the SPF standard explained

The SPF specification in plain British English: the record format, every mechanism, the four qualifiers, the eight result codes, the 10-lookup and void-lookup limits, and the macro language. Real example records throughout, plus exactly why SPF speaks for the return-path and never the visible From.

25 March 2026 · 13 min read

RFC 7208: the SPF standard explained

Sender Policy Framework, the thing you set up as a single TXT record that begins with v=spf1, is defined in RFC 7208. That document is the authority. It replaced the older RFC 4408 in 2014, and a small follow-up, RFC 7372, tidied up a couple of edge cases afterwards. Most people never read it, copy a record from a vendor instead, and then wonder why mail starts failing or why a checker reports permerror. This article walks through what RFC 7208 actually says, in plain British English, so that when you look at an SPF record you understand precisely what each character does and why the receiver behaves the way it does.

We will cover the record format, every mechanism, the four qualifiers, the eight result codes, the two processing limits that catch almost everyone out, and the macro language that hardly anyone uses but the standard insists exists. Where it helps, there are real records in code blocks you can adapt.

What SPF actually checks

Read this part carefully, because it is the single most misunderstood thing about the protocol. RFC 7208 defines a check on the envelope sender, not the address your recipient sees in their mail client.

When a sending server connects over SMTP, it issues a MAIL FROM: command containing a return-path address. That address, specifically its domain, is what SPF evaluates against. The standard calls this the MAIL FROM identity. There is a second identity, the HELO/EHLO hostname, that SPF also checks, and the standard requires receivers to check HELO when MAIL FROM is empty (the empty MAIL FROM used by bounce messages). The domain in the visible From: header, the one a human reads, is the header-from and SPF does not look at it at all.

That gap is exactly why DMARC exists. DMARC layers alignment on top, requiring the SPF-checked domain to match the header-from domain. So SPF on its own authenticates the bounce address, and you need alignment to make it mean anything about the visible sender. If you only remember one thing from RFC 7208, remember that SPF authorises the return-path, never the header-from.

The check itself is simple to state: given a sending IP address, a MAIL FROM domain, and a HELO name, is this IP authorised to send for that domain? SPF answers by fetching the domain's policy from DNS and evaluating it left to right.

The record format

An SPF policy is a single DNS TXT record on the domain. RFC 7208 retired the dedicated SPF resource record type (type 99) that the old standard had defined, so a TXT record is now the only correct place for it. Publish it once, as one string, on the domain whose MAIL FROM you want to protect.

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

The rules the standard sets out:

  • The record must start with exactly v=spf1, case-insensitive, followed by a space or the end of the record.
  • A domain must publish at most one SPF record. If a receiver finds two or more records that begin with v=spf1, the result is permerror. This trips up people who keep an old record and add a new one beside it.
  • Terms are separated by single spaces and evaluated strictly left to right.
  • The record is read until a match is found or the terms run out.

Terms come in two flavours: mechanisms, which can match the sending IP, and modifiers, which set parameters and never match. We will take mechanisms first because they are where the work happens.

Qualifiers: the prefix that decides the verdict

Every mechanism can carry a qualifier, a single character in front of it that says what to return when that mechanism matches. RFC 7208 defines exactly four:

  • + Pass. The IP is authorised. This is the default, so mx and +mx mean the same thing.
  • - Fail. The IP is explicitly not authorised. A receiver may reject the message outright.
  • ~ SoftFail. A halfway state: not authorised, but do not reject for that reason alone. Usually the message is accepted and marked.
  • ? Neutral. The domain asserts nothing. Treat it as if no policy applied.

So in ~all the qualifier is ~ and the mechanism is all. The whole tone of your record lives in the qualifier on your terminal all. More on that choice below, and there is a dedicated glossary entry on the all qualifier if you want the short version.

The mechanisms

Evaluation walks the mechanisms left to right. The first one that matches the sending IP determines the result, using that mechanism's qualifier. If nothing matches, evaluation continues until all or the end of the record.

all

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

all always matches. It is the catch-all that ends almost every record, and its qualifier sets the policy for any IP not matched earlier. -all says "everything else fails", ~all says "everything else softfails", ?all is neutral, and +all authorises the entire internet, which you should never publish.

Anything written after all is dead, because all always matches and stops evaluation. A common mistake is ~all include:somevendor.net, where the include can never run.

ip4 and ip6

These match the sending IP directly against an address or CIDR range. No DNS lookup involved, which makes them the cheapest mechanism you can use.

v=spf1 ip4:198.51.100.25 ip4:203.0.113.0/24 ip6:2001:db8::/32 -all

If the CIDR prefix length is omitted, it defaults to a single host (/32 for IPv4, /128 for IPv6).

a

Matches if the sending IP is one of the A or AAAA records of the named domain. With no domain given, it uses the MAIL FROM domain being evaluated.

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

You can attach a CIDR length with a double-colon dual syntax, a:example.com//64 for IPv6 or a:example.com/24 for IPv4, though in practice this is rare.

mx

Matches if the sending IP is one of the hosts listed in the domain's MX records. This is handy when your outbound mail leaves from the same hosts that receive it. Note that mx costs you the MX lookup plus a lookup per MX host, and the standard caps the number of MX hosts it will resolve at 10.

v=spf1 mx -all

include

The workhorse. include:domain fetches and evaluates that domain's SPF record as a subroutine. If that nested evaluation returns Pass, the include matches. Crucially, the qualifier on an include only governs what happens on a Pass from the nested record. An include that returns Fail, SoftFail or Neutral does not stop the outer evaluation; it simply does not match, and processing carries on to the next term.

v=spf1 include:_spf.google.com include:sendgrid.net include:_spf.example-crm.com -all

This recursive design is why SPF records nest so deeply and why the lookup limit, covered below, is the thing that breaks real-world records.

exists

Matches if the named domain has any A record at all. The point is not the address, it is whether the name resolves. Combined with macros it becomes a powerful per-sender lookup, used for things like positive-reputation systems. On its own it is uncommon.

v=spf1 exists:%{ir}.%{l}._spf.example.com -all

ptr (deprecated, do not use)

ptr matches by doing a reverse DNS lookup on the sending IP and checking whether the resulting hostname falls under the domain. RFC 7208 explicitly deprecates it: it is slow, it loads the .arpa DNS infrastructure, and the results are unreliable. The standard says receivers should still support it for compatibility but senders should not publish it. Treat its presence in any record you inherit as a smell.

Modifiers: redirect and exp

Modifiers set parameters rather than matching IPs. RFC 7208 defines two, and says unknown modifiers must be ignored rather than treated as errors.

redirect

redirect=domain replaces the current record with the SPF record of another domain, but only if no mechanism in the current record has matched. It is meant for organisations that want one canonical policy shared across many domains.

v=spf1 redirect=_spf.example.com

The trap: a redirect is only used when the record contains no all mechanism. If you write ... -all redirect=..., the all wins and the redirect is ignored. Use one or the other, not both.

exp

exp=domain names a record whose TXT content provides a human-readable explanation string that a receiver may surface when it returns Fail. It is optional and many receivers ignore it.

Result codes: the eight outcomes

This is the heart of RFC 7208 for anyone reading reports or diagnosing failures. An SPF evaluation returns exactly one of these eight results.

  1. none. No SPF record was found, or the domain does not exist. The receiver got no policy to apply.
  2. neutral. A record matched with the ? qualifier. The domain explicitly declines to assert anything. By design, this must be treated the same as none.
  3. pass. A mechanism matched with +. The IP is authorised for the MAIL FROM domain.
  4. fail. A mechanism matched with -, usually -all. The IP is explicitly not authorised. Also called "hardfail".
  5. softfail. A mechanism matched with ~, usually ~all. Probably not authorised, but the domain is not confident enough to demand rejection.
  6. temperror. A transient problem, almost always a DNS timeout or SERVFAIL. The check could not complete; the receiver may try again later. We have a whole walk-through of this in fix SPF temperror.
  7. permerror. A permanent problem with the record itself: two SPF records, broken syntax, or the lookup limit exceeded. The policy cannot be evaluated as published. See the permerror glossary entry.
  8. The standard groups these as the complete set; there is no ninth outcome.

Two of these are domain-owner errors you can and should fix: permerror is your record being wrong, and persistent temperror is your DNS being flaky or your includes pointing at hosts that time out. The other six describe legitimate verdicts about a given IP.

A subtle but important rule: a none or neutral result is not a pass. DMARC treats both as a non-passing SPF result, so a domain with no SPF record gets no SPF credit toward DMARC, full stop.

The processing limits that break real records

Here are the two numbers from RFC 7208 that cause more outages and permerror results than anything else combined.

The 10 DNS-lookup limit

During a single SPF evaluation, the total number of mechanisms and modifiers that cause a DNS query must not exceed 10. Cross that line and the result is permerror, which under a strict DMARC policy means the message can be rejected.

The mechanisms that count toward the limit are: include, a, mx, ptr, and exists, plus the redirect modifier. Each one counts as one, and includes are recursive, so an include that itself contains three includes consumes four of your ten. The mechanisms that do not count are ip4, ip6 and all, because they require no DNS query.

v=spf1 include:_spf.google.com include:servers.mcsv.net include:sendgrid.net include:_spf.example-crm.com -all

That record looks innocent. It has four includes, but each provider's record nests its own includes. Google's _spf.google.com alone expands to three further includes. It is genuinely easy to sail past ten without realising, especially as you add tools over the years. This is the single most common reason a working SPF record silently breaks: a vendor changes their nested record and your total tips over the edge.

Two further sub-limits live inside this one. The mx mechanism will resolve at most 10 MX hosts, and the ptr mechanism at most 10 PTR names; exceeding either is treated as a non-match rather than an error, but it is one more reason to avoid ptr and oversized mx.

The standard answer is SPF flattening: resolving all those nested includes into a static list of ip4 and ip6 ranges, which cost zero lookups. The catch is that flattened lists go stale the moment a provider changes their IPs, so a flattened record needs monitoring. Our SPF flattening glossary entry and the SPF record guide go into the trade-offs. To see your live lookup count, run your domain through the SPF checker.

The void-lookup limit

The second, less famous limit: RFC 7208 says an evaluation should fail with permerror after two "void lookups". A void lookup is a DNS query that returns an empty answer, either NXDOMAIN (the name does not exist) or NOERROR with zero records. The intent is to stop a malicious or broken record from generating a flood of pointless queries. In practice it bites when an include points at a domain that has been decommissioned, so the lookup returns nothing. Two such dead includes and the whole record errors. There is a void-lookup glossary entry if you want the detail.

Record length

DNS has its own constraint that SPF inherits. A single character-string in a TXT record cannot exceed 255 octets, and the whole record, once assembled, should stay within reasonable DNS limits (a 512-byte UDP response is the classic boundary, though EDNS extends it). When a policy needs more than 255 characters you split it into multiple quoted strings inside the one record, and the resolver concatenates them with no separator:

"v=spf1 ip4:198.51.100.0/24 ip4:203.0.113.0/24 "
"ip4:192.0.2.0/24 include:_spf.example.com -all"

Note there is no space across the join unless you put one inside the quotes, which is a frequent source of subtle breakage.

Macros: the part nobody reads

RFC 7208 includes a small macro language, mostly used inside exists and redirect. A macro is written %{letter} and expands to a property of the current check. The common letters:

  • %{s} the MAIL FROM sender, full address.
  • %{l} the local part of the sender (before the @).
  • %{d} the sender domain.
  • %{i} the sending IP address.
  • %{ir} the sending IP reversed, dotted, used to build reverse-DNS-style lookup names.
  • %{h} the HELO domain.
  • %{v} the literal in-addr for IPv4 or ip6 for IPv6.

There are escapes too: %% is a literal percent, %_ a space, %- a URL-encoded space (%20). A typical real-world use, a per-sender authorisation check:

v=spf1 exists:%{ir}.%{v}._spf.example.com -all

For a connection from 198.51.100.25, that expands to a lookup of 25.100.51.198.in-addr._spf.example.com. If the name exists, the mechanism matches. Most domains never need macros, but they are part of the standard and you will occasionally meet them in the wild, so it is worth recognising the syntax rather than panicking.

Putting it together: reading a record like the RFC does

Walk this record exactly as a receiver would, for a connection from 35.190.247.13 with MAIL FROM: alerts@example.com:

v=spf1 ip4:198.51.100.0/24 include:_spf.google.com include:sendgrid.net -all
  1. ip4:198.51.100.0/24: is 35.190.247.13 in that range? No. Move on. Zero DNS lookups so far.
  2. include:_spf.google.com: fetch Google's record and evaluate it as a subroutine. Google authorises that IP, so the nested result is Pass, the include matches, and the qualifier is the default +. Result: pass. Evaluation stops here.

The sendgrid.net include and the -all are never reached, because evaluation stops at the first match. That left-to-right, stop-on-match behaviour is the whole engine. Lookup count for that evaluation: one for the Google include, plus however many Google's own record nests, which is why ordering your cheapest and most-used senders first is not just tidy, it can keep you under the limit.

Where SPF stops and the rest of the stack begins

RFC 7208 is deliberately narrow. It authenticates an IP against a MAIL FROM domain and returns one of eight results. It says nothing about the visible sender, nothing about message content, and nothing about what a receiver must do with a fail. Everything beyond that, holding the visible From accountable, surviving forwarding, and producing reports, is the job of DKIM and DMARC sitting on top.

That is also why SPF alone is not a finished setup. The standard gives you authorisation; DMARC turns it into enforcement and visibility. If you want to see where your domain sits today, the DMARC checker reads your full policy and the SPF checker counts your live lookups and flags permerror conditions before a receiver does.

The practical takeaway

RFC 7208 is shorter and more learnable than its reputation suggests. The mental model is small: one TXT record starting v=spf1, mechanisms evaluated left to right, the first match wins using its qualifier, and the whole thing answers a single question about the bounce address. The two limits, ten DNS lookups and two void lookups, are what actually break records in production, and a permerror from breaching them can sink your mail under a strict DMARC policy just as surely as a real spoof would.

Get the syntax right, keep the lookup count honest, end with -all once you are confident, and remember that SPF only ever speaks for the return-path. The visible sender is DMARC's job.

If you would rather not babysit nested includes, lookup counts and flattening drift by hand, that is exactly what our hosted SPF service does for you: a single record we manage, kept under the limit automatically, with monitoring that alerts you the moment a provider's change threatens to push you into permerror. Start by running your domain through the free SPF checker and see what the standard is doing on your behalf right now.

Share

See where your domain stands today

Run a free DMARC scan, then let us take you to enforced p=reject with no email outage.