SPF Include Chains Explained With Examples | spFlat!

SPF Include Chains Explained (With Examples)

Published September 3, 2026 · 6 min read · spFlat! Blog

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.

The common mistake: counting the 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.

MechanismCounts toward the 10?What the receiver does
include:example.comYes (1 + everything nested)Fetches that domain's SPF record and evaluates it
a / a:mail.example.comYesLooks up A/AAAA records
mxYesLooks up MX, then addresses for those hosts
redirect=other.comYes (replaces the rest of the check)Starts over on another domain's SPF record
ip4: / ip6:NoCompares 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.

Why this keeps changing: Cloud providers add and split netblocks. Last quarter's "Outlook is 2 lookups" can be 3 this quarter. If you count includes by hand once and never look again, the chain grows without anyone editing your DNS.

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 wroteTypical nested lookupsRunning total
include:_spf.google.com4 (Google + 3 netblocks)4
include:spf.protection.outlook.com2-36-7
include:servers.mcsv.net (Mailchimp)2-38-10
include:_spf.salesforce.com2-410-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.

🔴 The kicker: Adding HubSpot, SendGrid, or Zendesk "just one more include" is what pushes a record that was already at 9 into PermError. The new vendor is not the only problem. The chain was already almost full.

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:

  1. Look up the TXT record for your domain and find the v=spf1 string.
  2. List every include:, a, mx, ptr, exists, and redirect=.
  3. For each include, look up that domain's SPF record and repeat.
  4. Stop when a record contains only ip4: / ip6: (and all). 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 SPF
Or start your free 14-day trial →