SPF Include Chains Explained (With Examples)
Most SPF records look harmless. Four include: statements, a ~all at the end, and a DNS tool that reports the
syntax as valid. Then Gmail starts treating your mail as unauthenticated, and the logs say SPF PermError.
The missing piece is the include chain: every include: is a DNS lookup of another SPF record, and that record
usually contains more includes. Receivers count the whole tree, not the four tokens you typed.
In this guide we'll unpack how include chains work, walk through three concrete examples, and show why a record that "only has four includes" can still burn through the 10-DNS-lookup limit in RFC 7208.
What is an SPF include chain?
An SPF record is a TXT record that lists authorized senders. The include: mechanism does not copy IP addresses into your
record. It tells the receiving server: go look up that other domain's SPF record and evaluate it as part of mine.
v=spf1 include:_spf.google.com ~all
That one line is not "Google's IPs". It is a pointer. The receiver fetches _spf.google.com, reads its SPF record, and
follows every mechanism it finds there. If Google's record includes three more domains, those count too. That nested walk is the
include chain.
include: tokens in your own TXT record. Receivers count every lookup the
chain triggers, including nested includes, a:, mx:, ptr:, exists:, and
redirect=. Direct ip4: and ip6: mechanisms do not count.
How the 10-lookup counter actually works
RFC 7208 caps the number of DNS-querying terms at 10 per SPF check. The initial lookup of your own SPF TXT record does not count. Everything that record causes does.
| Mechanism | Counts toward the 10? | What the receiver does |
|---|---|---|
include:example.com | Yes (1 + everything nested) | Fetches that domain's SPF record and evaluates it |
a / a:mail.example.com | Yes | Looks up A/AAAA records |
mx | Yes | Looks up MX, then addresses for those hosts |
redirect=other.com | Yes (replaces the rest of the check) | Starts over on another domain's SPF record |
ip4: / ip6: | No | Compares the sending IP locally, no extra DNS |
Once the 11th lookup is required, evaluation stops with a PermError. Remaining includes are never checked. A syntactically perfect record can still be a hard fail at runtime.
Example 1: One include that is really four lookups
Start with the smallest realistic record, Google Workspace only:
v=spf1 include:_spf.google.com ~all
On paper that is one include. The chain the receiver actually walks looks like this:
yourdomain.com
└── include:_spf.google.com (+1)
├── include:_netblocks.google.com (+1)
├── include:_netblocks2.google.com (+1)
└── include:_netblocks3.google.com (+1)
Google's top-level SPF record is itself a bundle of includes. Those nested records then publish ip4: and
ip6: ranges, which do not add further lookups. The bill for "just Gmail" is typically 4 lookups, not 1.
Vendor numbers drift over time. The point is the shape of the tree: one token in your record can hide several lookups you never typed.
Example 2: Microsoft 365 looks flat until you expand it
A Microsoft 365 include is the other classic "it should be cheap" assumption:
v=spf1 include:spf.protection.outlook.com ~all
Outlook's record has historically mixed direct IP blocks with extra include: statements for additional netblocks. A simplified
chain looks like this:
yourdomain.com
└── include:spf.protection.outlook.com (+1)
└── include:spfa.protection.outlook.com (+1)
└── further includes / mx as published
Depending on how Microsoft currently publishes the record, this is often 2-3 lookups rather than one. Combined with Google from Example 1 you are already around 6-7 lookups before you add a single marketing or CRM tool.
Example 3: A typical company record that silently exceeds 10
Now the record most mid-size teams actually publish:
v=spf1 include:_spf.google.com include:spf.protection.outlook.com include:servers.mcsv.net include:_spf.salesforce.com ~all
Four includes. Many DNS dashboards will even show a green check. Expand the chain with typical nested counts and the picture changes:
| What you wrote | Typical nested lookups | Running total |
|---|---|---|
include:_spf.google.com | 4 (Google + 3 netblocks) | 4 |
include:spf.protection.outlook.com | 2-3 | 6-7 |
include:servers.mcsv.net (Mailchimp) | 2-3 | 8-10 |
include:_spf.salesforce.com | 2-4 | 10-14 |
As a tree:
yourdomain.com
├── include:_spf.google.com (+1)
│ ├── include:_netblocks.google.com (+1)
│ ├── include:_netblocks2.google.com (+1)
│ └── include:_netblocks3.google.com (+1)
├── include:spf.protection.outlook.com (+1)
│ └── include:spfa.protection.outlook.com (+1)
├── include:servers.mcsv.net (+1)
│ └── nested Mailchimp includes (+1-2)
└── include:_spf.salesforce.com (+1)
└── nested Salesforce / ExactTarget includes (+1-3)
The 11th lookup never gets evaluated. If Salesforce is last in the record, some of your CRM mail may still pass on a lucky day while later mechanisms are skipped — or the whole check fails, depending on where the limit is hit. Either way, you no longer have a reliable policy.
A short chain you can count by hand
If you want to see this on a domain you control, start at the top and recurse:
- Look up the TXT record for your domain and find the
v=spf1string. - List every
include:,a,mx,ptr,exists, andredirect=. - For each include, look up that domain's SPF record and repeat.
- Stop when a record contains only
ip4:/ip6:(andall). Those leaves do not add lookups.
dig TXT yourdomain.com +short dig TXT _spf.google.com +short dig TXT _netblocks.google.com +short
Doing this fully by hand is tedious, which is why most people under-count. Online validators that stop at 10 lookups make it worse: they report a failure without showing the rest of the tree, so you never see which vendor put you over.
What flattening does to the chain
SPF flattening resolves the tree once, then publishes the resulting IP ranges directly. The include chain collapses to a short record that no longer asks receivers to recurse through Google, Microsoft, Mailchimp, and Salesforce on every inbound message.
Before (four includes, 10+ lookups):
v=spf1 include:_spf.google.com include:spf.protection.outlook.com include:servers.mcsv.net include:_spf.salesforce.com ~all
After flattening (one include, one lookup):
v=spf1 include:_yourdomain.spf.spflat.cloud ~all
Behind that single include, an automated flattener keeps the IP list current. When Google or Microsoft rotates ranges, the flattened record updates. You do not re-walk the vendor chain in your own DNS, and receivers do not pay the nested lookup cost.
Manual flattening can produce the same IP list once. It cannot keep the list true. That is the difference between a one-off
dig session and a flattening service.
How to inspect your own include chain
Use the free checker on spflat.cloud. It resolves nested includes and shows the lookup count the way a receiving server would, not the way your TXT record looks in a DNS panel.
If the number is 8 or 9, you are one new SaaS tool away from PermError. If it is already 10 or more, flattening is the fix, not another include.
🔍 See your include chain
Ten seconds. Free. Expand every nested include and see exactly how many lookups your SPF record consumes.
Check Your SPFOr start your free 14-day trial →
