An SPF record is a single DNS TXT record, starting with v=spf1, that lists the servers allowed to send mail for your domain. Receivers check the sending IP against it. RFC 7208 caps evaluation at 10 DNS lookups, and a domain may publish only one SPF record.
That is the whole idea. The difficulty is in the details: which mechanism costs a lookup, what the last term means once DMARC is in place, and how big senders keep their records under the limit. This guide works through each one with records we read with dig on 19 October 2026.
What an SPF record looks like
Here is Google's own record, read on 19 October 2026:
$ dig +short TXT google.com | grep spf
"v=spf1 include:_spf.google.com ~all"
Three parts. v=spf1 is the version and must come first. include:_spf.google.com says "also accept anything that record accepts". ~all is the catch-all at the end: anything not matched so far gets a softfail.
The record lives at the domain used in the envelope sender (the MAIL FROM, which ends up in the Return-Path header). It does not live at _spf or any special prefix unless you point to one with include or redirect.
A receiver reads the record left to right. The first mechanism that matches the connecting IP decides the result. Nothing after it is evaluated.
Mechanisms and qualifiers
Mechanisms
| Mechanism | Matches when | Costs a DNS lookup? |
|---|---|---|
ip4: / ip6: |
The sender's IP is in the listed address or range | No |
a / a:host |
The IP equals an A or AAAA record of the domain or host | Yes |
mx / mx:host |
The IP belongs to one of the domain's MX hosts | Yes |
include:domain |
The other domain's SPF check returns pass | Yes |
exists:domain |
The name (often built with macros) resolves to any A record | Yes |
ptr |
Reverse DNS of the IP ends in the domain | Yes, and RFC 7208 says it SHOULD NOT be published |
all |
Always matches; goes last | No |
Two modifiers sit beside them. redirect=domain hands the whole check to another domain's record, and it counts as a lookup. exp= points to an explanation string for rejections and does not count.
Qualifiers
Each mechanism can carry a prefix that sets the result when it matches:
+pass (the default if you write nothing)-fail~softfail?neutral
So -all means "anything not listed fails", ~all means "anything not listed is probably not us", and ?all says nothing useful. Never publish +all. It authorises every IP on the internet to send as you.
Real SPF records from large senders
These were read with dig +short TXT <domain> on 19 October 2026. Lookup counts come from following every include and redirect down to the bottom.
| Domain | SPF record (shortened where marked) | Lookups |
|---|---|---|
| google.com | v=spf1 include:_spf.google.com ~all |
1 |
| microsoft.com | v=spf1 include:_spf-a.microsoft.com include:_spf-b.microsoft.com include:_spf-c.microsoft.com include:_spf-ssg-a.msft.net include:_spf1-meo.microsoft.com -all |
7 |
| yahoo.com | v=spf1 redirect=_spf.mail.yahoo.com |
3 |
| github.com | v=spf1 ip4:192.30.252.0/22 include:spf.protection.outlook.com ... include:sendgrid.net ... ~all (8 includes) |
10 |
A few things stand out.
Google's _spf.google.com no longer nests further includes. It now holds the address ranges directly, so google.com spends one lookup in total. Microsoft splits its senders across five includes and ends with -all.
github.com sits exactly on the limit. Its eight includes (Microsoft 365, two Google netblock records, Zendesk, Salesforce, Mailchimp, Marketo and SendGrid) cost eight. Two more come from inside them: Salesforce's record uses exists:%{i}._spf.mta.salesforce.com, and SendGrid's record includes ab.sendgrid.net. That makes 10. One more SaaS tool added to that record would break SPF for every message.
github.com also shows a quirk of TXT records. Its record arrives as two quoted strings, split in the middle of an address: "... ip4:62.253.2" "27.114 ...". RFC 7208 section 3.3 says receivers must join the strings with no space, so this reads as ip4:62.253.227.114. If you split a long record yourself, split it the same way and never add a space at the join.
The 10 DNS-lookup limit and the 2 void-lookup limit
RFC 7208 section 4.6.4 sets two limits.
Ten terms that cause DNS queries. The include, a, mx, ptr and exists mechanisms and the redirect modifier all count, including those inside records you include. ip4, ip6 and all do not. Go over 10 and the receiver "MUST return permerror". Each mx mechanism also has its own sub-limit of 10 address lookups for the MX hosts it finds.
Two void lookups. A void lookup is a query that returns no answer or NXDOMAIN. The RFC says implementations SHOULD stop after two and return permerror. The usual cause is an include that points at a vendor you stopped using, whose SPF record has since been deleted. That stale include does more than waste a lookup: under the include rules in section 5.2, an included domain with no SPF record makes the whole check return permerror.
How to count lookups
Count by hand like this:
- Start at 0. Read your record.
- Add 1 for every
include,a,mx,ptr,existsandredirect. - For each
includeandredirect, fetch that record withdig +short TXT <name>and repeat steps 2 and 3 inside it. - Ignore
ip4,ip6andall.
For github.com that walk gives 8 at the top level plus 2 nested, which is 10.
Counting by hand gets tedious once vendors nest three deep. The SPF checker does the walk for you and shows the count per include, along with void lookups and duplicate records.
SPF flattening: what you gain and what you risk
Flattening replaces include: terms with the ip4: and ip6: ranges they currently resolve to. Address terms cost no lookups, so a record at 12 can drop to 2.
The trade-off is freshness. When you flatten, you copy the vendor's address list as it was today. Vendors change their ranges without telling you. Google's _spf.google.com alone listed one IPv4 /16, one IPv4 /17 and six IPv6 /56 ranges on 19 October 2026. If Google adds a range next month and your flattened copy does not, mail from that range fails SPF.
Other costs:
- Size. RFC 7208 section 3.4 says the DNS answer should fit in 512 octets. Flattened records grow fast.
- Macros cannot be flattened. Salesforce's
exists:%{i}...term is evaluated per sending IP, so there is no static list to copy. - Someone has to own it. A flattened record needs a scheduled job that re-resolves the includes and republishes on change.
Before flattening, try the cheaper fixes. Remove includes for tools you no longer use. Move a bulk sender to its own subdomain (for example mail.example.com) with its own SPF record, so it spends its own 10 lookups. And check whether a vendor needs SPF on your domain at all: many ESPs send with their own envelope domain and align through DKIM instead.
~all or -all when DMARC is in place
Our scan of the Tranco top 1,000 domains on 19 October 2026 found this split in the final SPF term:
| SPF ending | Domains (of 1,000) | Mail domains (of 688 with MX) |
|---|---|---|
-all (fail) |
430 | 339 |
~all (softfail) |
266 | 254 |
?all (neutral) |
19 | 13 |
redirect= only |
34 | 32 |
| No SPF record | 247 | 46 |
The large sites are split, and that is reasonable. DMARC only asks whether SPF returned pass for an aligned domain. Softfail and fail are both "not pass", so with DMARC at p=reject a spoofed message fails either way. Among the 346 mail domains at p=reject in the scan, 176 end SPF with -all and 146 with ~all.
The difference shows up before DMARC runs. RFC 7489 section 10.1 warns that some receivers apply SPF first, so -all "could cause that rejection to go into effect early in handling, causing message rejection before any DMARC processing takes place." Forwarded mail is the usual victim: the forwarder's IP is not in your record, SPF fails, and the message is rejected even though its DKIM signature would have passed DMARC. RFC 9989, the Standards Track DMARC specification published in May 2026 that obsoletes RFC 7489, keeps the warning in section 7.1 and adds that mail rejected this early "will never appear in aggregate DMARC reports".
A practical rule:
- No DMARC yet, or
p=none:-allgives receivers the clearer signal. - DMARC at quarantine or reject with DKIM signing everything:
~allis fine, and it avoids early rejection of forwarded mail. - Domains that never send mail:
v=spf1 -all, plus a DMARC record atp=reject.
Multiple SPF records: an instant permerror
RFC 7208 section 4.5 says that if more than one v=spf1 record remains after the lookup, the check "produces the permerror result". It does not merge them and it does not pick one.
This usually happens when a second vendor's setup guide says "add this TXT record" and someone adds it next to the old one. Three domains in our top 1,000 scan had exactly this problem on 19 October 2026.
The fix is one record that holds both sets of terms:
; wrong: two records
example.com. TXT "v=spf1 include:_spf.google.com ~all"
example.com. TXT "v=spf1 include:sendgrid.net ~all"
; right: one record
example.com. TXT "v=spf1 include:_spf.google.com include:sendgrid.net ~all"
Other TXT records on the same name (site verification tokens, for example) are fine. Only records that start with v=spf1 count.
Where SPF stops
SPF has two gaps you should know before relying on it.
It checks the envelope domain, not the From header people read. An attacker can pass SPF for their own domain while showing yours in From. Only DMARC alignment closes that gap; see the complete DMARC guide.
It also breaks on forwarding, because the forwarder's IP is not in your record. That is why DMARC accepts a pass from either SPF or DKIM. Sign everything with DKIM so forwarded mail still has a path to pass.
Gmail sets a floor too: its sender guidelines require SPF or DKIM for every sender, and both for anyone sending 5,000 or more messages a day to Gmail accounts. The sender requirements post covers the rest.
Next step
Run your domain through the SPF checker and write down the lookup count. If it is 8 or more, list each include and the team that owns it before you add another vendor. Then confirm the domain has a DMARC record with the DMARC checker, because SPF on its own says nothing about the From address your recipients see.