TechnicalReference

SPF Record Syntax: Every Mechanism Explained, Plus the Two Mistakes That Break Mail

A working reference for SPF record syntax: what each mechanism and qualifier does, why you can only have one record, and how the ten-lookup limit silently kills authentication.

August 4, 2026·9 min read·Flomailr Team

An SPF record is a single DNS TXT record that lists who's allowed to send mail using your domain. It looks like this:

v=spf1 include:_spf.google.com ~all

That's a complete, valid, working record. Most of the complexity people run into comes from two rules that aren't obvious from looking at one, so I'll cover the syntax first and then those rules, because the rules are what actually break things.

Where it lives

The record goes on your root domain as a TXT record, not on a subdomain and not on anything named _spf. Host field is usually @ or blank depending on your DNS provider. Check the result after saving:

dig +short TXT yourdomain.com
nslookup -type=TXT yourdomain.com

You want exactly one string beginning v=spf1.

The anatomy

Every record starts with the version tag v=spf1, and it's the only version that exists. Then a series of mechanisms, evaluated strictly left to right, and the first one that matches decides the outcome. Evaluation stops there. Order matters.

include: pulls in another domain's SPF record. This is what you use for sending platforms: include:_spf.google.com, include:sendgrid.net, include:amazonses.com. Your providers tell you which string to use. A match inside the included record counts as a match for yours.

a authorizes whatever IP your domain's A record points to. Bare a means your own domain; a:mail.yourdomain.com means that host. Useful if your web server sends mail, which is common with contact forms.

mx authorizes the servers listed in your MX records, on the theory that the machines receiving your mail probably also send it. Often true, not always.

ip4: and ip6: authorize addresses directly, either single (ip4:203.0.113.5) or in CIDR blocks (ip4:203.0.113.0/24). These cost you no DNS lookups, which becomes relevant shortly.

all matches everything and belongs at the end, always. It's the catch-all for any sender that didn't match anything before it.

Qualifiers, and why ~all versus -all matters

Any mechanism can carry a qualifier prefix. Without one, + is assumed.

+ passes, - fails hard, ~ soft-fails, and ? is neutral. You'll almost only ever see them on all.

~all means "anything not listed above probably isn't us, but deliver it and mark it." -all means "anything not listed above is not us, reject it."

The received wisdom is that -all is stricter and therefore better. In practice, if you're also running DMARC with an enforcing policy, the difference matters much less than people think, because DMARC is doing the deciding. And -all combined with an incomplete sender list is a good way to lose legitimate mail. Start at ~all. Move to -all once your DMARC aggregate reports show you've accounted for every real sender.

Never publish +all. It authorizes the entire internet to send as your domain, which is worse than having no record.

Rule one: you get exactly one record

Publish two TXT records that both start with v=spf1 and SPF doesn't merge them, doesn't pick the better one, and doesn't partially apply. It returns permerror, and a permanent error means the check fails. Every time.

This happens constantly, and the way it happens is predictable. You had Google Workspace, so you had a record. Then you signed up for a marketing platform, their setup wizard said "add this TXT record," you added it as instructed, and now there are two.

v=spf1 include:_spf.google.com ~all
v=spf1 include:sendgrid.net ~all

Both look right. Together they're broken.

The fix is to merge the mechanisms into a single record with one v=spf1 at the front and one all at the end:

v=spf1 include:_spf.google.com include:sendgrid.net ~all

Then delete the other record. Not edit it to be empty, delete it.

Worth being precise about one thing: this rule applies only to TXT records starting with v=spf1. Your domain verification strings, your DKIM records, your DMARC record at _dmarc, all of those are separate TXT records and none of them conflict with SPF. Only SPF records collide with SPF records.

Rule two: ten DNS lookups, and then it stops working

This is the one that catches people who did everything else right.

SPF caps the number of DNS lookups a receiver will perform while evaluating your record. The limit is ten. Go over it and the result is permerror, which means your SPF fails for every message, including from senders correctly listed in the record.

Which mechanisms count against the limit: include, a, mx, ptr, and exists. Each one is a lookup. What makes this vicious is that include is recursive, so an include that itself contains three includes costs you four, not one. Google's _spf.google.com expands to several. Some platforms cost three or four on their own.

What doesn't count: ip4, ip6, and all. They're resolved from the record itself with no further queries.

So a record that looks modest can quietly exceed the limit:

v=spf1 include:_spf.google.com include:sendgrid.net include:mailgun.org
  include:servers.mcsv.net include:_spf.salesforce.com ~all

Five includes, easily more than ten actual lookups once they expand, and SPF is now failing for a domain whose owner believes it's configured correctly. Nothing announces this. Mail keeps flowing, because SPF failure alone doesn't stop delivery, until you turn on DMARC enforcement and suddenly everything fails at once.

Check where you stand with any SPF validator that reports a lookup count.

Two ways out, and one of them is a trap.

The good fix is subtraction. Remove platforms you no longer use, and there are almost always some; that trial from last year is probably still in your record. If a provider publishes specific IP ranges, swapping include:theirdomain.com for ip4: entries removes a lookup at the cost of having to maintain the IPs yourself.

The trap is flattening. Flattening tools resolve every include down to raw IP addresses and hand you one long static record with zero lookups. It works on the day you generate it. Then your provider rotates their sending IPs, which they do without telling you, and your record still lists the old ones. Mail starts failing and nothing you changed caused it. If you flatten, you need automation that regenerates the record on a schedule, and at that point ask whether you'd rather just use fewer senders.

The thing SPF doesn't do

SPF checks the envelope sender, the Return-Path, not the From: address your recipient reads. Those are different fields and they frequently hold different domains.

A spammer can pass SPF perfectly while displaying your domain in the From: line, because they pass on their domain in the envelope. SPF, on its own, does not stop spoofing. It never claimed to.

DMARC is what closes that hole, by requiring that whatever passed also aligns with the visible From: domain. Which is why SPF alone is half a configuration, and why the sequence worth doing is SPF, then DKIM, then DMARC at p=none, then reports, then enforcement. If you've already published a DMARC record and a scanner is telling you the policy isn't enabled, that's the second half of this job waiting for you.

A record that's fine

For a small business on Google Workspace with one marketing platform:

v=spf1 include:_spf.google.com include:sendgrid.net ~all

Two lookups' worth of includes, room to spare under the limit, soft fail on the catch-all, one record. That's the whole thing. SPF records get long and fragile through accumulation, not because anyone needed them to be complicated, so the useful habit is deleting the include when you cancel the service.

If you take one thing from this: go check right now whether you have two SPF records. It costs you thirty seconds and it's the single most common way this gets broken.

Try Flomailr free

Ready to put this into practice?

Flomailr is built for small businesses, visual email builder, automation flows, and real analytics. Free up to 500 contacts, no credit card required.

Start free today
← View all posts