The SPF 10 DNS lookup limit, nested includes, and why flattening is risky
SPF has a hard ceiling that quietly breaks working email: a receiving server must give up after evaluating 10 DNS lookups in a single SPF check, and when your record needs more than that the result is PermError, which many receivers treat as a failure.
What counts toward the 10
The limit is set in RFC 7208. It counts mechanisms that trigger DNS resolution, not the total characters in the record. The mechanisms that cost a lookup are include, a, mx, ptr, and exists, plus the redirect modifier. The ip4, ip6, and all mechanisms cost nothing because they resolve nothing.
| Mechanism | Counts toward limit | Note |
|---|---|---|
include: | Yes | And everything it pulls in counts too |
a | Yes | Each a is one lookup |
mx | Yes | Plus one per MX host returned, up to a second cap of 10 |
ptr | Yes | Discouraged entirely, slow and unreliable |
exists: | Yes | |
redirect= | Yes | |
ip4: / ip6: | No | Inline addresses, free |
all | No | Always free |
Why nested includes overflow
The trap is that include is recursive. When you write include:_spf.google.com, the receiver fetches that record, and whatever includes it contains are counted against your budget too. A single vendor include often expands into three or four nested lookups.
v=spf1 include:_spf.google.com include:servers.mcsv.net include:sendgrid.net ~all
That looks like three lookups. In practice Google alone expands to several nested includes, and once you add a couple of marketing vendors you cross 10 without adding anything visible. The record still looks short. The failure only appears when a receiver counts the full expanded tree.
PermError is a real failure
A PermError is not a soft warning. Under a strict DMARC policy, SPF returning PermError means the SPF leg of DMARC does not pass, and if DKIM is not aligned the message fails DMARC and can be rejected. The worst part is that it is intermittent in appearance: the record that passed last month fails this month because a vendor added one more nested include on their side, with no change to your own record.
What flattening actually does
SPF flattening resolves every include, a, and mx ahead of time and bakes the final list of IP addresses directly into your record as ip4 and ip6 entries. Since inline addresses cost zero lookups, a flattened record can hold hundreds of ranges and still pass.
v=spf1 ip4:35.190.247.0/24 ip4:64.233.160.0/19 ip4:66.102.0.0/20 ... -all
Why flattening is risky
Flattening moves the problem rather than removing it. When a vendor changes their sending IP ranges, their own SPF record updates automatically and every normal include picks up the change. Your flattened record does not. It holds a frozen snapshot, so the day the vendor rotates IPs, legitimate mail starts failing SPF and you have no warning. A flattened record is only safe if something re-resolves and republishes it on a schedule, and that automation is now a piece of infrastructure you have to own and monitor.
A calmer fix is usually to prune. Remove vendors you no longer send through, drop ptr entirely, and consolidate where one provider covers several needs. Most records that overflow do so because of dead includes nobody removed.
When you are reworking a record, build it deliberately and check the lookup count before you publish with the SPF Generator.