SPF record syntax, term by term
Last updated 2026-08-28
An SPF record is one TXT record, read from left to right, and the first term that matches the connecting server ends the check. A syntax error anywhere in it returns permerror (4.6), which is a hard stop: no server in the record passes, including the ones listed before the mistake. Every message you really send then depends on its DKIM signature to pass DMARC. The record still reads as correct in your DNS panel.
SPF record syntax: v=spf1, then terms, then -all
What is the correct syntax for an SPF record? Publish one TXT record at the sending domain. It starts with v=spf1, then lists terms separated by spaces, and it ends with -all or ~all. Most terms name a source of mail: include: for a platform that sends for you, ip4: for a server you run yourself.
That record authorises Google Workspace to send as the domain, and -all asks receivers to refuse every other server. Google's own SPF setup page publishes the same line ending in ~all, which asks a receiver to accept an unlisted server and mark it instead.
Every rule on this page comes from RFC 7208, the SPF specification, and the section numbers are there so you can check me.
Placement: one record per domain or subdomain
The record goes at the exact domain that appears after the @ in the envelope sender, and not in a subdomain under it (RFC 7208 3). SPF stops at that exact name, so news.example.com needs a record of its own and never inherits the one at example.com. Microsoft's SPF setup docs say the same: "only one SPF record is allowed per domain or subdomain".
Check the record itself against 4 rules.
- TXT only. SPF had its own DNS record type for a few years, and RFC 7208 3.1 dropped it. If your DNS panel offers a record type called SPF, pick TXT instead.
- Exactly one. When two records at one name both start with
v=spf1, the check returns permerror and stops, and a receiver reads no term in either one (4.5). v=spf1comes first. A receiver keeps only the records that begin with that exact version string and discards the rest (4.5). A typo there costs you the whole record, and the result isnone.- Left to right, first match wins. A receiver tests each term in order, and the first one that matches the connecting IP address ends the evaluation with that term's qualifier (4.6.2). A receiver ignores anything written after an
allmechanism (5.1).
SPF checks the envelope sender that the sending server gives in the MAIL FROM command, plus the HELO name, and it never reads the From: header your recipient sees (2.4, 2.5). A platform can pass SPF for its own bounce domain while your name sits in the From line, which is what DMARC alignment exists to catch.
To read what your DNS is serving, ask a resolver rather than the DNS panel:
Qualifiers set the result of a match
Every mechanism can carry a qualifier in front of it. The qualifier decides what a match returns.
| Qualifier | Result | What it says |
|---|---|---|
| + | pass | This host is authorised. The default when you write no qualifier. |
| - | fail | This host is not authorised. |
| ~ | softfail | This host is probably not authorised. |
| ? | neutral | The record makes no assertion about this host. |
The qualifier is optional and defaults to +, so mx and +mx are the same term (4.6.2). What a receiver does with each result is in the results list below.
DMARC narrows the choice, because it passes only when SPF or DKIM passes for an aligned domain (RFC 7489 6.6.2). pass is the only SPF result that counts for you there. Microsoft's setup page puts the two endings side by side: "DMARC treats -all (hard fail) and ~all (soft fail) as SPF failures. But the DMARC policy is effectively ignored for SPF ~all failures if the messages don't also contain DKIM signatures". Receivers can also act on a hard fail before DMARC runs at all, since RFC 7489 10.1 notes that -all may cause "message rejection before any DMARC processing takes place".
The all mechanism decides the result only for servers your record does not list, so a server you did list passes under either ending. The ending decides what a receiver does with a server you left out. Under -all the receiver fails it, and DMARC then rests on whether that platform signs with your domain. Publish ~all while you are still finding your senders, get DKIM signing in place on every platform, then tighten to -all once the sender list stops changing.
Mechanisms: 8 of them, 5 cost a lookup
RFC 7208 defines 8 mechanisms. Each one either matches or it does not, and 5 of them query DNS (4.6.4).
all
all matches every host, so it belongs at the end as the default answer (5.1). A domain that sends no mail gets the shortest useful record there is, and RFC 7208 10.1.2 recommends exactly this for a parked domain:
Lookups: 0, because all matches without asking DNS anything.
include:
include:example.net runs the whole check again against another domain's record, and it matches when that second check comes back pass (5.2). Only the result crosses over. A -all inside the included record does not end your evaluation and does not make your result a fail. RFC 7208 5.2 calls the name "poorly chosen" for that reason: only the evaluated result of the referenced record is used.
Lookups: 1, and every querying term inside the included record lands on your budget too.
a
a matches when the connecting IP address is one of the A or AAAA addresses of a domain (5.3). Written bare, it resolves the domain being checked, which on most domains is the web server. With an argument it resolves that name instead, and a CIDR length widens the comparison to a block:
Before you keep it, check that the name sends mail. A bare a on a domain whose A record points at your web server spends a lookup on a machine that runs no mail server.
Lookups: 1, whether you write a domain after the mechanism or leave it bare.
mx
mx matches when the connecting IP address belongs to one of the MX hosts for a domain (5.4). The syntax mirrors a, so mx:example.com and a CIDR length both work:
mx authorises the hosts in your MX records, so it earns its place only when the same machines send your outbound mail. RFC 7208 4.6.4 also caps the lookups behind the term: "the evaluation of each MX record MUST NOT result in querying more than 10 address records". That cap sits apart from the 10-term budget, and a check that trips it returns permerror.
Lookups: 1 term against the limit of 10, plus the address records that term resolves.
ip4: and ip6:
These two compare the connecting address against a literal address or a CIDR block, and like all they query nothing (5.6, 4.6.4).
An ip4: value with no prefix length means /32, and an ip6: value with no prefix length means /128. You may not abbreviate the address instead of writing the prefix: 192.0.2.0/24 is correct and 192.0.2 is a syntax error (5.6). So is a hostname. I fed v=spf1 ip4:mail.example.com -all to pySPF and it answered PermError SPF Permanent Error: Invalid IP4 address: ip4:mail.example.com.
Lookups: 0, however many addresses and blocks you list.
exists:
exists: builds a domain name, looks it up, and matches if any A record comes back (5.7). A receiver never compares that address to the connection, so only the existence of the record decides. Paired with macros it can authorise per user or per address:
Lookups: 1, and a macro inside the name makes that lookup different for each connection. I have rarely seen the mechanism outside large sending platforms.
ptr
RFC 7208 titles section 5.5 "ptr" (do not use) and says the mechanism SHOULD NOT be published. The note in that section calls it slow and a large burden on the .arpa name servers. Receivers still have to support it, so a ptr in your record spends a lookup and buys you a mechanism the specification asks you to drop. Delete it.
Lookups: 1 term, spent on the reverse lookup and an address lookup for each name it returns.
Modifiers: redirect and exp
A modifier is a name and a value joined by =. Modifiers never match or fail to match; they carry information instead (4.6.3). RFC 7208 defines 2. Either one twice in a record returns permerror (6).
redirect=example.net hands the decision to another domain's record once none of your mechanisms have matched (6.1). An include: asks a narrow question, "does that record say pass", and the evaluation carries on when the answer is no. A redirect= makes the other record's result your result, including its all, and a missing record at the target returns permerror rather than none. A receiver ignores redirect= whenever your record also contains an all mechanism, wherever that mechanism sits in the record (5.1, 6.1). A record carrying both keeps the all and drops the redirect.
Lookups: 1, before the record at the far end spends anything of its own.
RFC 7208 6.1 shows the case it is built for, 3 domains sharing one policy:
Google runs its own consumer domain this way. dig +short TXT gmail.com returned v=spf1 redirect=_spf.google.com when I ran it on 2026-08-20.
exp= names a TXT record holding a sentence a receiver may quote back when your record fails a message (6.2). That lookup happens after the fail rather than during evaluation, so it spends none of your budget (4.6.4).
Receivers must ignore unknown modifiers too (6). Mistake 2 below shows what that costs a record.
Macros expand parts of the message
A macro is a %{ } sequence inside a term argument, and it expands at evaluation time to something from the connection. %{i} becomes the client IP address, %{d} the domain being checked, and %{l} the local part of the sender, with a handful more listed in 7.2. A macro can also reverse and split its value, which is how exists:%{ir}.%{l1r+-}._spf.%{d} turns one term into a lookup per user and per address. RFC 7208 7 has the full grammar. If you are publishing a record for a company that sends through a mailbox provider and two platforms, you need none of it.
Length: 255 characters per string
A DNS character-string holds at most 255 octets. Publish a longer value as several strings, and receivers concatenate them with nothing added, so "v=spf1 inc" "lude:example.net -all" reads as one record (3.3). Some managed DNS panels do that split for you and others refuse a value over 255 characters. Check with dig after saving either way.
The specification asks that the whole answer stay inside 512 octets and gives 450 octets of records as the working number (3.4). Watch the other TXT records at the same name too, since they share the answer: sendgrid.net was serving 10 other TXT strings alongside its SPF record on 2026-08-20. You hit the 10-lookup limit long before you hit 450 octets, and a record that needs splitting is usually a record carrying more includes than it can afford.
Mechanism and modifier names are case-insensitive (4.6.1), and so are the domain names inside them. Publish everything lowercase anyway. When I gave V=SPF1 MX -ALL to Kitterman's checker, it answered "SPF records must start with v=spf1, this does not appear to be a valid SPF record". The ABNF makes that string legal and a working implementation refused it, which is the argument for lowercase.
Results: 7 outcomes, and 1 that helps DMARC
An SPF check returns one of 7 results (2.6). Each one is a verdict about the connecting server.
- none. The receiver found no record, or nothing at that name started with
v=spf1. The receiver has no statement from you to apply. - neutral. Your record declined to assert anything about the host. Receivers must treat it exactly like none (8.2).
- pass. The server is authorised. This is the only result DMARC can use (RFC 7489 6.6.2).
- fail. The server is not authorised. A receiver that rejects here should answer
550 5.7.1(8.4). - softfail. The server is probably not authorised. Receivers should not reject on this alone (8.5).
- temperror. A DNS error interrupted the check.
451 4.4.3if the receiver rejects, and a retry may succeed with no change from you (8.6). - permerror. The receiver could not interpret the record.
550 5.5.2if the receiver rejects, and it stays broken until somebody edits DNS (8.7). The SPF permerror page is the triage version.
Lookups: 10 per evaluation, permerror past it
include, a, mx, ptr, exists and redirect= each query DNS during evaluation. all, ip4:, ip6: and exp= do not (4.6.4). Those querying terms are capped at 10 for the whole evaluation, counting everything nested inside every include, and a receiver that passes 10 returns permerror rather than a partial answer. Receivers should also stop after 2 lookups that come back empty and return permerror (4.6.4). The 10-lookup guide counts a real record term by term and gives the 4 ways to win lookups back. On a paid plan, the sending routes check lines each include up against 90 days of your own reports and gives it a keep, review or remove verdict.
Mistakes that break a record
Run down this list against the record you are about to save. Each one takes a DNS edit to fix and then a TTL to clear, so check the record before you hit save.
- A second record next to the first. Two
v=spf1records at one name return permerror (4.5). Adding a platform means editing the record you have, not pasting a new one beside it. include=instead ofinclude:. A term with=in it is a modifier,includeis not a defined modifier, and receivers must ignore unknown modifiers (6). Sov=spf1 include=spf.protection.outlook.com -allraises no error and meansv=spf1 -all. I evaluated that record through pySPF from 198.51.100.7 and it answeredFAIL Message may be rejected. Microsoft's common syntax errors table lists the same typo.- A space after the colon.
include: spf.protection.outlook.comsplits into two broken terms. pySPF answeredPermError SPF Permanent Error: empty domain:: include:, with the doubled colon and a trailing space of its own. - A hostname where an address belongs.
ip4:andip6:take literals and CIDR blocks only (5.6). Usea:for a name. - A bare
allor a+allending. The qualifier defaults to+, and RFC 7208 A.1 spells out whatv=spf1 +allmeans: any host passes. Anyone on the internet can then send as your domain. - A record with no
all. A record with no default ends neutral, as though?allwere written there (4.7), and receivers handle neutral exactly like a missing record (8.2). Finish the record. - Something in front of
v=spf1. The version section has to open the record, or the receiver discards it and returns none (4.5). - A record type called SPF. RFC 7208 3.1 discontinued type 99. Publish TXT.
- A trailing period after an include domain. Microsoft's table lists
include:spf.protection.outlook.com.as an error, and pySPF accepted the same record when I tested it. RFC 7208 7.3 settles it: conforming implementations MUST accept a trailing dot, but trailing dots SHOULD NOT be published, because some older implementations reject them. Drop the dot.
Google Workspace plus SendGrid, in one record
Build it in the order the mail leaves.
Start with the mailbox provider, because it carries the staff mail. Google's setup page gives include:_spf.google.com. Add the platform next: SendGrid's SPF documentation gives v=spf1 include:sendgrid.net -all, and the include is the part you take. Both go in one record, because a receiver that finds two records returns permerror for both. Then add any server you run yourself as a literal, since an address you control costs no lookup and cannot be renumbered without you knowing. Finish with the ending you can defend.
Now count what it spends. The ip4: literal is free. When I resolved the two includes against 8.8.8.8 and 1.1.1.1 on 2026-08-20, they cost 3 lookups between them. _spf.google.com cost 1, because the record held only ip4: and ip6: terms that day. sendgrid.net cost 2, because its record includes ab.sendgrid.net.
Older counts put _spf.google.com at 4, from the structure that nested three _netblocks records inside it. All three still resolve on their own, and the parent record no longer includes them. Any lookup count you read, on this page or on anybody else's, is a measurement from the day somebody resolved the chain, so resolve it yourself before you save the record.
Syntax checkers, and what each one answers
3 free tools answer the 3 questions worth asking before you save the record, and none of them needs an account.
- Is the syntax valid? Kitterman's SPF record check runs a record string you have not published yet through pySPF, the Python SPF library. The site belongs to Scott Kitterman, who wrote RFC 7208. I checked every record on this page with it.
- What does the syntax mean? open-spf.org's syntax tables hold the community reference, and RFC 7208 itself reads better than its reputation suggests.
- What does it cost, live? Our SPF checker resolves the whole include chain against real DNS and totals it against the limit of 10.
Missing senders break a valid record
None of the 3 tools above knows which servers send as you, and that is the half of the job that breaks mail. A record can satisfy every rule on this page and still leave out the invoicing app that a contractor connected, or the helpdesk that started mailing customers last quarter. Under -all, a receiver fails those servers.
DMARC aggregate reports name every source that sent as your domain and say whether SPF and DKIM passed, one XML file per receiver per day. A rua= tag on your DMARC record turns them on, and the DMARC setup guide has the record. Two outside tools turn those files into something a person can read, at no cost. Cloudflare's DMARC Management charts each sending source against its SPF and DKIM results, on any Cloudflare plan, for a domain whose nameservers already point at Cloudflare. If yours do, the record you just edited and the reports on it sit behind one login, and I would start there. Postmark's DMARC Digests emails a summary every week to any domain, whoever runs its DNS. When I last checked, the free version listed the top 10 sources with 5 IP addresses each and held 7 days of history. The paid version cost $14 a month per domain, and it covered every source, 60 days of history, a dashboard and logins for a team.
DomainCanary is our product, so read this paragraph as the pitch it is. We take the XML from every receiver that sends it, and a file with a broken record inside it still counts: our parser drops that record and keeps the valid ones around it. One email a week names each source and its verdict. The paid plans also mail you between those weekly emails, and the trigger is narrow: a source nobody has seen on your domain before, failing on the day its first report lands. A sender you already know about, failing this week for the first time, waits for the weekly email. Pro watches 5 domains for $19 a month, gathers them into one email, and sends at most one alert a day per domain. Every plan keeps at least 12 months of source detail, the free one included.
A report gives us an IP address and the domains it authenticated as, so a source you do not recognise still costs you a search. A receiver writes the first report a day or two after you publish the rua= tag, and the first digest follows that file. Once every real sender shows up passing, you change one character: ~all becomes -all. Get the weekly digest, free for your first domain.
Publish the record with ~all this week and read a month of reports before you touch the ending. Each mistake on this page takes one line of DNS to fix. You will spend the month working out which servers belonged in the record in the first place.
Run the ramp on real reports
Every step in this guide depends on a few weeks of aggregate reports. We parse them and mail you a weekly summary: the senders that used your domain, what failed, and when tightening the policy is safe. Paid plans email you the day a new source first fails. Your first domain is free.
Get the weekly digestNo card · 12+ months of history · The free plan does not expire
Questions
Can a domain have two SPF records?
No. When a receiver finds more than one TXT record starting with v=spf1 at the same name, SPF returns permerror and stops. Nothing in either record passes, and the domain stays broken until somebody merges the two into one. RFC 7208 section 4.5 defines that. Adding a second sending platform means editing the record you already have.
What is the difference between -all and ~all at the end of an SPF record?
A minus sign makes an unlisted server a fail, and a tilde makes it a softfail, which the RFC describes as a weaker statement that receivers should not reject on by itself. DMARC treats both as SPF failures, since only an SPF pass counts for DMARC. Use ~all while you are still identifying senders, then tighten to -all.
Does the order of mechanisms in an SPF record matter?
Yes. A receiver evaluates terms left to right and the first one that matches the connecting server ends the check with that term's qualifier. Nothing written after an all mechanism is ever tested. Put your cheapest and most common sources early, and put all last.
How long can an SPF record be?
A single DNS string holds 255 characters, and a longer record is published as several strings that receivers join together with no space added. RFC 7208 also asks that the whole DNS answer fit in 512 octets. In practice the 10-lookup limit stops a record long before its length does.
Keep reading
Checking as you go? The DMARC checker reads the policy you are ramping, the SPF and DKIM checkers show whether your senders will survive it, and the report analyzer reads an aggregate report you already have. No signup.
DomainCanary is a DMARC monitoring service that turns your domain's aggregate reports into one weekly email.