SPF · PermError species 2 · DNS
“Multiple SPF records found” — the other PermError, and the merge that fixes it
SPF's rule is absolute — the specification's own words: a domain name “MUST NOT have multiple records.” Find two, and the receiver's defined answer is an error, not a choice between them. Your years of one-record-per-tool have been quietly voiding each other.
The fast answer
- Diagnose precisely (next section): two records beginning
v=spf1at the same name — not two quoted pieces of one record, not records at different subdomains. - Worksheet: list every mechanism from both records; attribute each to a tool; drop the dead; keep one ending (
all) — chosen deliberately. - Publish in safe order: screenshot everything → edit one record into the merged record → delete the others in the same session. Never delete-first.
- Verify: one
v=spf1at the name, checker clean, and a test message from each real sender passing SPF.
Diagnose: records vs strings (the 60-second distinction)
Run a TXT lookup for your root domain (any DNS checker). Read the answer like this:
| What the lookup shows | Verdict |
|---|---|
Two separate answers, each starting v=spf1 | The error. Two records. Merge them. |
One answer whose value appears as two quoted chunks: "v=spf1 …" "… -all" | One legal record. Long values are stored as multiple strings and joined (with no spaces added) by design. Do not “fix” this. |
One record at the root, another at mail. or another subdomain | Also legal. SPF is per-name; a sending subdomain carries its own record. The merge in this guide concerns one name only. |
A value starting v=spf10 or similar near-miss | Not an SPF record at all — the spec's own example of what gets discarded during selection. It doesn't cause this error, and it protects nothing. |
A warning about vocabulary: some vendor help pages (Cloudflare's included) describe string-splitting as “splitting into multiple records.” When the panel language and the lookup disagree, believe the lookup.
The merge worksheet
- Copy both records into a document. Every
include:,ip4:,a,mx— line them up. - Attribute each mechanism to its owner. The include names usually confess (
_spf.google.com→ Workspace;spf.protection.outlook.com→ Microsoft 365; a vendor domain → that tool). Unknown mechanism + no tool you still pay for = candidate dead sender. - Drop the provably dead. If you run DMARC reports, they are the evidence: a sender that hasn't appeared in a full reporting period isn't sending. No reports? Keep the mechanism this round — deletion is next quarter's job, with data.
- Choose the ending once. Both records'
allqualifiers collapse to one. The vendors themselves disagree — Google recommends~alland calls-allpotentially “overly restrictive”; Microsoft publishes-all. Our rule: provably complete inventory →-all; any doubt →~allnow, tighten after a clean reporting period. (+all or ?all in a legacy record never survives a merge — they authorise the world.) - Budget-check the union: count the merged record's lookup cost with the method in our lookup-limit guide. Merging fixes the count error; it can create the other PermError if the union is fat.
v=spf1 include:_spf.google.com include:spf.protection.outlook.com include:<your-tool> ~allOne record. One beginning. One ending. Every living sender inside.
Bad merges — exhibited, so you don't ship one
| Botched merge | Why it fails |
|---|---|
| Pasting record 2 after record 1 inside one value | Everything after the first all mechanism is dead text by the spec's grammar — the second tool is silently unauthorised, and the stray second v=spf1 mid-value is a syntax error that can keep the PermError alive. |
| “Merging” a subdomain's record into the root | Different name, different country. Subdomain records stay where they live. |
| Deleting all records, then adding the merged one “fresh” | Between delete and add, lookups return none — a window where SPF simply doesn't exist. (One vendor's onboarding doc advises delete-and-regenerate inside its own walled garden; generalised, it's the most destructive version of this fix.) Edit-then-delete has no window. |
| Deleting the other TXT records in the tidy-up | Verification tokens and service proofs share the name. The sweep in this guide touches records beginning v=spf1 only. |
“But my mail has been working for years…”
Probably — and the RFC itself explains why, in its own appendix: the result of multiple records is deterministically PermError, but the consequence is each receiver's local policy. Some reject, some deliver with the error annotated, and DMARC can still pass on the DKIM leg — carrying mail that SPF has formally disowned. “Mostly working” and “invalid” have coexisted in your DNS for years; enforcement trends are why it surfaces now, typically as one major receiver starts honouring the letter of the spec.
FAQ
Which record should survive as the one I edit?
The most complete one — fewest changes between it and the merged union. Sentiment and age are not criteria; mechanism coverage is.
How long until the error clears?
As caches expire — typically within your TTL, with full global consistency inside 24–48 hours in the worst case. Verify by fresh lookup, not by clock.
I merged, and now one tool fails SPF.
Its mechanism didn't survive the merge (typo, a dropped include, or the dead-text merge above). Diff the live record against your worksheet — the worksheet existing is what makes this a two-minute repair instead of an archaeological dig.
Sources
RFC 7208 — §3.2 (the MUST NOT), §4.5 (selection and the permerror), §3.3 (multi-string records), §4.4 (per-name lookup), §8.7 & Appendix G.3 (result vs consequence) · Google Workspace Admin Help — SPF guidance (~all recommendation) · Microsoft Learn — SPF records (-all posture) · Cloudflare documentation (string-splitting wording, flagged as found).
Did this fix work for you?
Your reaction tells us whether this guide solved the problem it promised to solve.