CertaDNS

SPF flattening: what it fixes, what it breaks, and when to use it

Flattening replaces includes with literal IP ranges to get under the lookup limit. It works, and it converts a hard failure into a silent one unless it is automated. The trade-off, stated honestly.

Last reviewed

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
Abbreviated. A real flattened record for three providers commonly runs to several hundred characters.

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.

IncludesFlattened
LookupsOne each, recursivelyZero
Provider IP changePicked up automaticallyRecord is now wrong
Failure modeLoud — permerror when you exceed a limitSilent — a subset of mail quietly fails SPF
MaintenanceNoneOngoing, and it must be automated
Record sizeSmallLarge, 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 -all

At 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

SituationDo this
Under 10 lookups with headroomNothing. Leave the includes alone — this is the mechanism working as designed.
At 9 or 10, few sendersRemove what you no longer use first. It is usually enough, and it costs nothing to maintain.
Over the limit, senders are stableFlatten — with automated regeneration. Never a one-off paste.
Over the limit, senders change oftenMacros, or move some senders to a subdomain with its own record and its own ten.
Flattened record approaching 512 bytesStop 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.

Check this on your own domain

Free, no account, reads public DNS only.

Related

CertaDNS automates most of what is described above — Email Authentication. The manual route above works perfectly well; the product exists because keeping it correct as your senders change is the part that does not stay done.