The record is in the Cloudflare dashboard. The syntax validates. Mail still lands in spam or gets rejected for failing SPF. On Cloudflare this almost always comes down to one of three things, and the reason it is hard to spot is that none of them are wrong in the DNS editor — the record is fine, but something else is either duplicating it, redirecting what it points at, or preventing it from being served at all.
What the Failure Looks Like
# In the Authentication-Results header of a delivered message
spf=fail (sender IP is 203.0.113.25)
smtp.mailfrom=example.com
# Or, when there are two records
spf=permerror (SPF Permanent Error: too many SPF records)
# In a bounce
550 5.7.23 The message was rejected because of Sender Policy Framework violationThe distinction matters. permerror points at cause one below — the record set itself is invalid. fail means the record was read successfully and the sending IP simply was not in it, which points at cause two.
Cause 1: Email Routing Published a Second SPF Record
Turning on Cloudflare Email Routing adds MX records and its own SPF record to the apex, typically v=spf1 include:_spf.mx.cloudflare.net ~all. If your domain already had an SPF record for Google Workspace, Microsoft 365, or a transactional provider, the apex now answers with two.
SPF has no merge behaviour. RFC 7208 is explicit: if more than one record matches, the result is a permanent error — so publishing two records is strictly worse than publishing one, and worse than publishing none.
$ dig +short TXT example.com
"v=spf1 include:_spf.mx.cloudflare.net ~all"
"v=spf1 include:_spf.google.com ~all"
# Two records starting with v=spf1 -> permerror, every message failsThe fix is to delete one and fold its mechanisms into the other:
v=spf1 include:_spf.mx.cloudflare.net include:_spf.google.com ~allIf you delete Cloudflare's SPF record while Email Routing is still enabled, the dashboard may flag the configuration as incomplete and offer to restore it — which recreates the duplicate. Edit the existing record to include_spf.mx.cloudflare.net rather than deleting it, so the requirement stays satisfied by a single record.
Cause 2: Your a and mx Mechanisms Resolve to Cloudflare
This is the one that is genuinely specific to Cloudflare, and the one people rarely find on their own.
The a mechanism means "pass if the sending IP matches an A record for this domain." The mx mechanism means the same thing for the A records behind your MX hosts. Both work by resolving hostnames at evaluation time — and if those hostnames are proxied (the orange cloud), what resolves is not your server. It is Cloudflare's anycast edge.
# Your record
v=spf1 a mx ~all
# Your real mail server
203.0.113.25
# What the A record returns because the hostname is proxied
$ dig +short A example.com
104.21.34.12
172.67.181.7
# SPF authorizes the Cloudflare edge, not your mail server -> failMail leaving your own server at 203.0.113.25 is now unauthorized by your own record. Nothing in the SPF syntax is wrong, and no validator will complain, because the record is valid — it simply authorizes the wrong addresses.
Fix it by naming the sender explicitly instead of inferring it from proxied DNS:
# Instead of a / mx, state the actual sending IP
v=spf1 ip4:203.0.113.25 ~all
# Or delegate to the provider that sends for you
v=spf1 include:_spf.google.com ~allThe obvious-looking fix — putting the proxy addresses into the record — authorizes address space shared by every Cloudflare customer. Any of them could then pass SPF as your domain. Use ip4: for your own server or include: for your mail provider.
A related trap: an MX record must point at a hostname that is not proxied. If the A record behind your MX host has the orange cloud enabled, inbound mail breaks as well as SPF. Set that hostname to DNS-only.
Cause 3: The Zone Is Not Authoritative
Records saved in Cloudflare only matter if Cloudflare is the answer to an NS query for your domain. Two situations break that:
- Nameservers never changed at the registrar. The Cloudflare overview page shows the zone as pending. Records save normally and are served to nobody.
- Partial (CNAME) setup. On this mode Cloudflare is not authoritative for the zone at all — your original DNS host still is. TXT records added in Cloudflare are inert, and your SPF record has to be published at the real provider.
# Is Cloudflare actually serving this zone?
$ dig +short NS example.com
carl.ns.cloudflare.com.
dana.ns.cloudflare.com.
# Ask Cloudflare directly, bypassing every cache
$ dig +short TXT example.com @carl.ns.cloudflare.comIf the second command returns your record but a normal lookup does not, delegation is the problem, not the record.
Confirm delegation with the NS Lookup tool
Two Smaller Cloudflare-Specific Traps
Quotes pasted into the Content field
Cloudflare quotes TXT values for you. Pasting a value that already carries quotation marks stores them as characters, so the record no longer begins with v=spf1 and stops being an SPF record entirely — while still looking correct at a glance in the dashboard.
The ten-lookup limit
include:_spf.mx.cloudflare.net consumes one of the ten DNS lookups SPF permits, and each nested include inside your other providers consumes more. Adding Email Routing to a record that was already close to the limit tips it into permerror — the same symptom as the duplicate record, from a different cause.
The SPF Validator expands the full include tree and shows the running count, which is the only practical way to see where the budget went.
Confirming the Fix
# Exactly one record, and it starts with v=spf1
$ dig +short TXT example.com
"v=spf1 include:_spf.mx.cloudflare.net ip4:203.0.113.25 ~all"
# Then send a test message and read the receiving header
Authentication-Results: mx.google.com;
spf=pass (google.com: domain of you@example.com designates
203.0.113.25 as permitted sender)A single record and spf=pass from a real delivery is the only confirmation that counts. A validator can only tell you the record parses.
Frequently Asked Questions
1. Does the Cloudflare proxy strip or block TXT records?
No. TXT records are always served as-is and the proxy toggle does not apply to them. The proxy affects SPF only indirectly: if your SPF record uses a or mx, those mechanisms resolve A records that may be proxied, which returns Cloudflare addresses instead of your mail server.
2. Why did enabling Email Routing break my SPF?
Email Routing publishes its own SPF record at the apex. If you already had one, the domain now answers with two records starting with v=spf1, which is a permanent error under RFC 7208 rather than a merge. Combine them into a single record.
3. Can I just add Cloudflare IP ranges to my SPF record?
No. Those addresses are shared across every Cloudflare customer, so you would be authorizing an enormous pool of unrelated senders to send as your domain. Replace a and mx with an ip4: for your actual mail server, or an include: for your mail provider.
4. My record shows in the Cloudflare dashboard but dig returns nothing.
The zone is not authoritative. Either the registrar still points at the old nameservers (the Cloudflare overview will say the zone is pending), or the zone is on a partial CNAME setup where Cloudflare serves only specific records. Check delegation with the NS Lookup.
5. Is ~all instead of -all why my mail is failing?
Almost never. A softfail means unlisted senders are marked, not rejected, so ~all is more permissive than -all and cannot be the cause of a hard failure. If mail is being rejected on SPF grounds, the sending IP is not authorized by any mechanism in the record.
6. How long should a change take to appear?
Cloudflare serves edits within seconds. Anything longer is a resolver holding the previous answer until the old TTL expires. Query one of your Cloudflare nameservers directly to see the current truth without the cache in the way.