Cover image for Your SPF record can be valid, published, and completely ignored

Jose Pollman

There's a failure mode in SPF that produces no error anywhere you'd think to look.

Your record is syntactically valid. dig returns it. Your DNS provider is happy. Your own test mail arrives fine. And yet some of your mail is failing authentication at the receiver, and nothing in your infrastructure will tell you.

The cause is usually that you've gone past ten DNS lookups.

Ten is a hard ceiling, not a guideline

RFC 7208 section 4.6.4 requires evaluators to cap DNS-querying terms at ten. Go past it and the result is PermError — a permanent failure.

The important part: receivers don't degrade gracefully. They don't evaluate the first ten and shrug at the rest. They treat the entire record as unusable. You go from "SPF configured" to "SPF absent" in one step, and the only place that's visible is in DMARC aggregate reports, which most people aren't reading yet.

Six terms cost a lookup. Four are free.

Costs a lookup Free
include: ip4:
a ip6:
mx all
ptr exp=
exists:
redirect=

The count isn't yours to control

This is what catches people out. The limit applies to the whole resolution tree, not just your record.

You publish four includes. One of those vendors publishes six includes of their own. You're at eleven, and your record lists four things.

Nothing changed on your side. A vendor updated their record and broke yours. There is no notification for this, from anyone, ever.

Counting it by hand

You can walk the tree with dig if you want to see it directly:

dig +short TXT example.com | grep spf1

Enter fullscreen mode Exit fullscreen mode

Then for every include: you find, recurse:

dig +short TXT _spf.vendor.com | grep spf1

Enter fullscreen mode Exit fullscreen mode

Keep going until you bottom out, counting every include:, a, mx, ptr, exists: and redirect= along the way.

It's tedious but instructive — you'll usually find one vendor accounting for half your budget.

Three traps that make tools lie to you

Doing this by hand or with a naive script, there are three places to go wrong. I've seen all three produce confidently incorrect output.

1. redirect= uses =, not :

It's a modifier, not a mechanism, and both humans and regexes skip it. Plenty of production domains are nothing but a redirect:

hubspot.com.  TXT  "v=spf1 redirect=_hspf.hubspot.com"

Enter fullscreen mode Exit fullscreen mode

Miss that single term and you conclude the domain uses zero lookups — when in fact every include and every cost lives behind it. If a checker tells you a domain like this uses no lookups, the checker is broken.

2. Void lookups have a separate budget

A lookup that returns nothing is a void lookup, and RFC 7208 caps those at two.

So an include pointing at a vendor you stopped paying for last year, whose record no longer exists, hurts you twice: once against the ten, and once against the void limit. Dead includes aren't free just because they resolve to nothing.

3. TXT records arrive in pieces, joined with nothing

This is the one that bites anyone writing their own tooling.

DNS transmits TXT records as one or more character-strings of at most 255 bytes. A long SPF record arrives in several chunks, and RFC 7208 section 3.3 says they're concatenated with no separator.

Real records split mid-token. One production record ends a chunk with ...ip4 and starts the next with :161.38.192.0/20.

Join them correctly and you get ip4:161.38.192.0/20. Join them with a space — the obvious-looking thing to do — and you get ip4 :161.38.192.0/20, which isn't valid syntax.

If you're parsing SPF yourself, handle this first. If you're using someone else's tool and the output looks subtly mangled, this is usually why.

Fixing it, in the order worth trying

Remove senders you no longer use. The only fix with no downside, and it's almost always available. Most domains over the limit are carrying includes for tools nobody has logged into in years, and deleting one buys back its entire subtree. Get the include list in front of whoever owns marketing and billing and ask what's still live.

Ask the vendor for a narrower include. Some publish a broad include covering all their infrastructure plus a tighter one scoped to a product or region. This is rarely documented — you have to ask support.

Flatten, but as little as possible. Replacing include:vendor.com with the IPs it resolves to turns a five-lookup subtree into zero, because ip4: and ip6: are free.

The cost is permanent and easy to underestimate: you've taken on that vendor's maintenance burden. When they add a sending IP, their record updates and yours doesn't. Your mail from that provider starts failing SPF silently, and you find out when someone notices deliverability dropped a month ago.

If you flatten:

  • Only flatten enough to get under ten. Retiring the single most expensive include is often sufficient, and leaves everyone else free to rotate their own IPs.
  • Never flatten a provider using macros. A term like exists:%{i}._spf.example.com expands against the connecting IP. There's no fixed set of addresses to inline, and flattening it silently drops every sender it would have matched.
  • Re-check monthly, in a calendar, not in your head.

Worth knowing: large providers increasingly flatten their own records for this reason. _spf.google.com today contains only ip4: and ip6: entries with no nested includes, so include:_spf.google.com costs you exactly one lookup rather than four.

Don't aim for ten

Ten is legal. Ten is not a target.

A record sitting at exactly ten breaks the day someone in marketing signs up for one more tool, and nobody will connect those two events. Aim for seven or eight so there's headroom to add a sender without causing an outage.


I maintain notspoofed, a free checker that walks the whole include tree, follows redirect=, counts against the real ceiling, and generates a corrected record that flattens only what it has to. It handles all three traps above, because I got them wrong first. No signup, and the domains you check aren't logged.

If you've hit this in an interesting way — particularly a vendor whose record silently blew your budget — I'd like to hear about it.