surface

Learn · RFC 7208

SPF

The list of servers allowed to send mail as your domain.

SPF (Sender Policy Framework) is a DNS TXT record that lists which mail servers are authorised to send mail on behalf of your domain. When a receiving server gets a message claiming to be from yourdomain.com, it looks up your SPF record, checks whether the connecting server's IP address is on the authorised list, and passes that result on to DMARC to decide what to do with it. SPF on its own doesn't reject or quarantine anything — it just answers a yes/no question that DMARC then acts on.

The record

v=spf1 ip4:203.0.113.10 include:_spf.google.com include:sendgrid.net -all

A record is a sequence of mechanisms, each evaluated left to right, plus a qualifier in front of each:

  • + (pass, the default if no qualifier is given) — authorised.
  • - (fail) — explicitly not authorised. Ends the record; this is what -all means.
  • ~ (softfail) — probably not authorised, but receivers are told to accept it and mark it, not reject it outright.
  • ? (neutral) — no assertion either way. Functionally close to having no rule at all.

The mechanisms themselves:

  • ip4: / ip6: — a specific address or CIDR range. Free — doesn't count against the lookup limit below.
  • a — authorises the domain's own A/AAAA record. Costs one lookup.
  • mx — authorises whatever the domain's MX records point to. Costs one lookup, plus one more per MX record returned.
  • include: — pulls in another domain's SPF record and evaluates it inline. This is how third-party senders (Google Workspace, SendGrid, your CRM) get authorised. Costs one lookup, plus whatever the included record itself costs, recursively.
  • exists: — a DNS existence check, rarely used directly. Costs one lookup.
  • all — matches everything. Always the last mechanism, and the qualifier in front of it is the entire point of the record.

The 10-lookup limit — the part most checkers get wrong

RFC 7208 caps SPF evaluation at 10 DNS-querying mechanisms per lookup, counted recursively through every include:. This limit exists to stop SPF evaluation from being turned into a denial-of-service vector, but in practice it's the single most common way a perfectly reasonable-looking SPF record silently stops working. Each include: doesn't cost one lookup — it costs one lookup for the include: itself, plus however many lookups the included record uses, and if that record includes another one, those costs stack too. A record that reads cleanly as five include: statements can easily total 15 or 20 actual lookups once you follow the chain all the way down, because commercial email platforms often include several layers of their own infrastructure.

v=spf1 include:_spf.google.com include:sendgrid.net include:spf.protection.outlook.com include:_spf.salesforce.com include:mailgun.org -all
# looks like 5 lookups — is very likely well over 10 once each include
# is actually resolved and its own includes are counted

Exceeding 10 lookups doesn't just fail the specific mechanism that pushed it over — it makes the entire record return a PERMERROR, and most receivers then treat the domain as if it had no SPF record at all. This means a domain can have a record that looks complete and correct, list every legitimate sender, and still provide zero protection, with no visible error anywhere except in the mechanics of DNS resolution. Surface counts every lookup recursively through every include: chain and flags a record once it's within two of the limit, not just once it's already broken — because by the time it's actually broken, adding one more sending service is what tips it over, and that's usually how someone finds out the hard way.

The other limit: void lookups

RFC 7208 also caps void lookups — mechanisms that return neither an error nor any usable data, typically because a referenced domain doesn't exist or has no record of the expected type — at 2 for the whole evaluation, separately from the 10-lookup total. This one is rarer in practice because it usually means a typo (a misspelled include: target) or a leftover reference to infrastructure that's since been decommissioned, rather than an organic side effect of adding legitimate senders. But it produces the exact same silent PERMERROR as the main limit, which makes it worth knowing about specifically: a record can be well under 10 real lookups and still fail entirely because two of its mechanisms point at domains that no longer resolve.

Common failure modes

  • No SPF record — anyone can claim to send from the domain with no server-side check at all.
  • +all — explicitly authorises every server on the internet. Occasionally used during testing and never removed; functionally identical to having no SPF at all, but looks intentional.
  • Exceeding the 10-lookup limit — see above. The most common real-world SPF failure, and invisible unless something checks recursively.
  • Two SPF records at the same name — as invalid as two DMARC records, for the same reason: receivers can't determine which one applies and typically discard both.
  • ~all left in place indefinitely — a reasonable transitional setting, but a domain that's confident in its sender list should move to -all rather than leaving spoofed mail merely flagged instead of rejected.

If a record is close to the limit, the fix is to flatten low-value includes into their underlying ip4:/ip6: ranges (a static list, but one that costs zero lookups) rather than adding another include: on top.

Is your domain actually covered?

Surface checks SPF — and everything else on this page — against your real DNS and headers, free, in seconds.

Scan your domain →