Flattening replaces the include: terms in an SPF record with the IP ranges they resolve to. Since ip4: and ip6: cost no DNS lookups, a flattened record can list dozens of senders and still evaluate in zero. It is the standard answer to the ten-lookup limit, and it works.
It also changes the shape of the failure. Worth understanding before you commit, because the new failure mode is quieter than the one you are fixing.
What flattening does
Before — three lookups, one for each include:
v=spf1 include:_spf.google.com include:sendgrid.net include:servers.mcsv.net -all
After — zero lookups, the ranges written out:
v=spf1 ip4:74.125.0.0/16 ip4:209.85.128.0/17 ip6:2001:4860:4864::/56 ip6:2404:6800:4864::/56 … ip4:167.89.0.0/17 ip4:168.245.0.0/17 … -all
Nothing about how receivers evaluate it changes. The record authorises the same hosts. It is purely a matter of whether the resolution happens at publish time or at delivery time.
What it costs
An include: is a live reference. When Google adds an IP range, every domain that includes _spf.google.com picks it up on the next message, with no action from anyone. That is the entire value of the mechanism.
A flattened record is a snapshot. When Google adds a range, your record does not know, and mail from the new range fails SPF. And here is the part that matters: it fails quietly. There is no error, no alert, nothing in your logs. A subset of your mail starts failing SPF, DMARC starts leaning entirely on DKIM, and if your policy is at enforcement and DKIM is missing on that path, the mail is rejected.
| Includes | Flattened | |
|---|---|---|
| Lookups | One each, recursively | Zero |
| Provider IP change | Picked up automatically | Record is now wrong |
| Failure mode | Loud — permerror when you exceed a limit | Silent — a subset of mail quietly fails SPF |
| Maintenance | None | Ongoing, and it must be automated |
| Record size | Small | Large, and bounded by DNS |
Flattening by hand is a trap
A record flattened once and pasted into DNS is correct on the day it is created and decays from then on. If you are going to flatten, the regeneration has to be automated and it has to run more often than your providers change — which for the large senders means at least daily.
The other ceiling: record size
Flattening trades a lookup limit for a length limit. A single DNS TXT string is capped at 255 characters; longer records are published as multiple strings that the receiver concatenates, which is fine. The practical ceiling is the UDP response size — once a response exceeds 512 bytes, resolvers fall back to TCP or EDNS0, and while both work, some paths handle it poorly.
A domain with enough senders can flatten itself past that ceiling and end up with a different intermittent failure. If your flattened record is heading for a kilobyte, the answer is fewer senders or a subdomain, not a bigger record.
The alternative: macros
SPF supports macro expansion, which allows a record to be evaluated against the connecting IP rather than enumerating every possibility:
v=spf1 exists:%{i}._spf.example.com -allAt evaluation time %{i} expands to the connecting IP, so the receiver queries something like 203.0.113.5._spf.example.com. Answer the query for authorised addresses and not for others, and you have an authorisation check that costs one lookup regardless of how many senders you have — and that is live rather than a snapshot.
This is how dynamic SPF products work, including ours. The trade is that it needs a DNS backend capable of answering a synthesised query per address, which is not something a normal zone file can do.
- Cost: one lookup, fixed, however many senders.
- Freshness: live — the answer is computed when the query arrives.
- Requirement: a nameserver that can respond dynamically. You cannot do this with a static zone.
When to use which
| Situation | Do this |
|---|---|
| Under 10 lookups with headroom | Nothing. Leave the includes alone — this is the mechanism working as designed. |
| At 9 or 10, few senders | Remove what you no longer use first. It is usually enough, and it costs nothing to maintain. |
| Over the limit, senders are stable | Flatten — with automated regeneration. Never a one-off paste. |
| Over the limit, senders change often | Macros, or move some senders to a subdomain with its own record and its own ten. |
| Flattened record approaching 512 bytes | Stop flattening. Split across subdomains or reduce senders. |
Worth saying plainly: dmarcian, among others, argues against flattening entirely on the grounds that it breaks the mechanism's intent, and that position is defensible. The counter-argument is that a record at permerror is failing right now, and an automated flattened record that is occasionally a few hours stale is better than one that is permanently broken. Both can be true; which applies depends on whether the automation is real.
Either way, start by finding out where you actually stand — the SPF Inspector shows the count and which includes are responsible for it.