← Blog/Authentication

Building an SPF record: syntax, limits and the lookup budget

October 15, 2026·7 min read·By MailPilot

A complete SPF record is one TXT record at your domain's apex, containing a version tag, a list of mechanisms naming the servers allowed to use your domain as the envelope sender, and a default for everything else:

v=spf1 include:_spf.google.com include:spf.protection.outlook.com ~all

That is the whole format. A generator assembles this line from checkboxes, which is useful as a typo-check and useless for the two things that actually break SPF records: the DNS lookup budget, and knowing which services still send as you.

Mechanisms and what each one costs

MechanismAuthorisesDNS lookups
include:domaineverything that domain's SPF record authorises1, plus everything inside it
a or a:hostthe A/AAAA records of the domain or host1
mx or mx:domainthe hosts in those MX records1
ip4:203.0.113.9one address0
ip4:203.0.113.0/24a CIDR range0
ip6:2001:db8::/32an IPv6 range0
exists:domaina macro-expanded existence check1
ptrhosts whose reverse DNS matches — deprecated1, slowly
redirect=domainreplaces this record entirely1
alleverything not already matched0

Each mechanism carries a qualifier that tells the receiver what to do when it matches:

  • + pass — the default, so mx and +mx are identical.
  • ~ softfail — accept but mark. Correct while you are still discovering senders.
  • - hardfail — reject. Correct once DMARC reports show nothing legitimate is missing.
  • ? neutral — explicitly no opinion. Almost never useful.

~all and -all are the only sensible endings. +all authorises the entire internet to send as you and some receivers score it as a spam signal on its own.

The 10-lookup limit, counted properly

SPF permits a maximum of 10 DNS-querying mechanisms per evaluation. The count is recursive: each include costs one, plus every lookup inside the record it points at, plus every lookup inside *those*. Exceeding it returns PermError, and PermError means SPF fails for all your mail at once — not partially, not just for the last include.

There is a second, less-known limit: at most two "void" lookups, meaning queries that return no answer. An include pointing at a service that no longer exists is not merely useless; two of them put you over a limit.

Count by hand. Expand each include:

dig +short TXT _spf.google.com
dig +short TXT spf.protection.outlook.com

A record containing further includes contributes all of them. Large providers commonly cost three or four lookups each, which is why four ESP includes are usually over budget before you add your own mx and a.

How to get back under it, in the order to try:

  1. Remove services you no longer use. This fixes most over-budget records on its own.
  2. Delete `a` and `mx` if those hosts do not send mail. They are the most common dead weight — your web server and your inbound mail hosts usually send nothing.
  3. Delete `ptr`. Deprecated, slow, and ignored by some receivers.
  4. Move services to their own subdomains. mg.yourdomain.com for Mailgun, news.yourdomain.com for marketing. Each subdomain gets its own record and its own fresh budget of 10, and the apex keeps room for your mailbox provider.
  5. Only then consider flattening includes into ip4: ranges. Flattening works and costs nothing in lookups, but you have just taken permanent responsibility for tracking a provider's IP changes. A stale flattened record fails silently, which is worse than a record that fails loudly.

The rules that invalidate a record

  • One `v=spf1` record per name. Two is a specification violation: receivers return PermError and ignore SPF entirely. This is the most common broken-record case and it usually happens when two teams each add "their" record.
  • `v=spf1` must come first. No leading whitespace, no other tag before it.
  • One record, split into quoted strings if long. A TXT string is limited to 255 characters; a long record is published as multiple quoted strings in the same record and concatenated by the resolver. Two separate records is a different, broken thing.
  • No nested `redirect` and `all` together. redirect= is only consulted when no mechanism matched, and an all mechanism always matches, so a record containing both never reaches the redirect.
  • SPF belongs at the apex and at any subdomain that sends. Subdomains do not inherit the apex record. A subdomain with no SPF record of its own has no SPF, which is why a v=spf1 -all record on non-sending subdomains is worth publishing.

Audit an inherited record in five minutes

dig +short TXT yourdomain.com | grep spf1        # exactly one line expected
dig +short TXT yourdomain.com @8.8.8.8           # compare against a public resolver
dig +short TXT <each include target>             # expand and count

Then ask, for each mechanism: which service is this, and does it still send as us? Anything you cannot name should be removed — after checking DMARC reports to confirm no legitimate traffic depends on it.

What SPF cannot do for you

SPF validates the envelope sender, not the From header your recipient sees. It breaks on forwarding, always, because the forwarder is not in your record. And a passing SPF check contributes nothing to DMARC unless the authenticated domain *aligns* with the From domain — so a platform sending with its own bounce domain gives you a pass that DMARC ignores. For hosted sending, aligned DKIM is usually what carries DMARC, and SPF is the backup.

What not to do

  • Do not add every provider's include "to be safe". Count first.
  • Do not use a generator's output without expanding the includes and counting lookups.
  • Do not publish a second SPF record. Merge, always.
  • Do not leave ptr or stale includes in place.
  • Do not set -all before DMARC reports show you have found every sender. Go ~all first.
  • Do not treat SPF as the whole job. Publish DKIM and DMARC too; SPF alone is the weakest of the three.

MailPilot checks SPF, DKIM and DMARC on every connected mailbox's sending domain and re-checks as you send, so a record that drifts over the lookup limit or picks up a duplicate shows up as a failing check instead of as a slow decline in placement.

MailPilot

Ready to reach the inbox every time?

Automated email warmup across real peer mailboxes. Live inbox placement monitoring. Free 7-day trial - no credit card required.

Start free trial
Reach the Inbox, Every Time
Connect unlimited inboxes for warmup. Free 7-day trial, no credit card.
Try for Free
WORKS WITH
OZOHO
inbox placement99.2%
free trial7-day
companies trust us340+

Start for free - no credit card required.

Get started freeBook a demo