Multiple SPF Records: Why They Cause PermError and How to Fix Them
If a domain publishes more than one TXT record beginning with v=spf1, SPF evaluation returns permerror. Receivers do not combine the policies, choose the strictest one, or use the first one they happen to see.
The fix is to inventory every legitimate sender and publish one SPF record for that domain. Do not blindly paste the two strings together. SPF evaluates mechanisms from left to right, and the first match ends evaluation, so an accidental merge can change which systems are authorized.
One detail causes a lot of bad diagnoses: multiple TXT records are normal, and one TXT record may itself contain multiple character-strings. Only multiple SPF records are the problem.
Which DNS shape actually causes the error?
These examples use the reserved .test domain and documentation IP addresses. They are hypothetical.
# Fine: several TXT records, but only one is SPF
example.test. TXT "google-site-verification=abc123"
example.test. TXT "v=spf1 ip4:192.0.2.10 -all"
# Fine: one SPF TXT resource record made of two strings
example.test. TXT "v=spf1 include:_spf.example.test " "-all"
# Broken: two separate TXT resource records both select as SPF
example.test. TXT "v=spf1 include:_spf-a.example.test -all"
example.test. TXT "v=spf1 include:_spf-b.example.test ~all"
RFC 7208, section 4.5 tells an SPF evaluator to query TXT records, discard records that do not begin with the v=spf1 version marker, and then count what remains. Zero matching records means none. One can be evaluated. More than one is permerror.
The second example above still contains one TXT resource record. DNS stores its value as two character-strings, which RFC 7208, section 3.3 says to concatenate without adding spaces. This representation is useful because a single DNS character-string cannot exceed 255 octets. The space before the closing quote in the first chunk is therefore intentional.
That distinction is easier to preserve when you inspect the response as records and chunks rather than copying flattened terminal output.
How can you detect multiple SPF records in code?
In Node.js, dns.resolveTxt() returns an outer array of TXT resource records. Each record is an inner array of character-strings. Join each inner array first, then select the SPF records.
import { resolveTxt } from "node:dns/promises";
async function inspectSpf(domain) {
const rows = await resolveTxt(domain);
const textRecords = rows.map((parts) => parts.join(""));
const spfRecords = textRecords.filter((record) =>
/^v=spf1(?: |$)/i.test(record)
);
if (spfRecords.length === 0) {
return { status: "none", spfRecords };
}
if (spfRecords.length > 1) {
return {
status: "permerror",
reason: "multiple_spf_records",
spfRecords,
};
}
return { status: "single", record: spfRecords[0] };
}
console.log(await inspectSpf("example.com"));
I ran the classifier against five fixed fixtures: an unrelated TXT record beside one SPF record, a split SPF string, two SPF records, a lookalike version token, and a mixed-case version marker. All five checks passed. The fixture deliberately classifies record shape only. It is not a full SPF evaluator and does not decide whether a particular sender passes.
You can also inspect a domain with the site’s SPF checker. It shows the published SPF record, terminal policy, includes, and redirect target. Treat that as domain-level configuration evidence, not proof that a mailbox exists or that a message will reach the inbox.
Why do receivers return permerror instead of merging the records?
Two policies can disagree in ways that have no safe automatic answer.
v=spf1 include:_spf-app.example.test -all
v=spf1 include:_spf-marketing.example.test ~all
Should the receiver use -all or ~all? Which include should run first? What happens if the first include matches an address that the other policy was meant to exclude? A DNS client may also return resource records in an order the publisher did not intend.
SPF avoids those ambiguities by requiring a domain to publish no more than one selectable SPF record. The result is permanent because the published policy itself is invalid. Retrying the same message does not resolve it.
A fair objection is that both records may authorize legitimate platforms, so surely merging them is obvious. Combining the sources may be the right repair, but only the domain owner can make that decision. The receiver cannot infer ownership, intended order, or the correct terminal policy from two conflicting declarations.
How do multiple SPF records get published?
The usual cause is configuration split across people or tools. A mail provider asks for an SPF value, someone creates it, and a later service adds a second TXT record instead of editing the existing policy. DNS control panels often allow this because multiple TXT records are valid DNS. The SPF constraint applies only after the evaluator selects records beginning with v=spf1.
Another common trap is confusing a split character-string with two records. A control panel might display one long SPF value as several quoted pieces. If those pieces belong to one TXT resource record, that is fine. If the panel shows two separately editable rows, each with its own v=spf1, it is not.
Finally, check the exact domain used for SPF evaluation. SPF normally evaluates the SMTP MAIL FROM domain, with HELO used in the cases described by the protocol. Repairing example.com does not fix mail whose envelope sender is bounces.example.com.
How should you consolidate two SPF records safely?
I use a source inventory before touching DNS:
- Export both current SPF records exactly as published.
- Map every
include,ip4,ip6,a,mx, andredirectterm to an owned system or approved provider. - Confirm which systems still send with this exact envelope-sender domain.
- Remove obsolete authorizations. Do not carry old services into the new policy for convenience.
- Choose one terminal
allpolicy deliberately. - Construct and review one replacement record, then check its total DNS-triggering terms.
- Publish the replacement through one controlled DNS change.
For example, suppose both includes are still required and the domain owner chooses a hard-fail terminal policy. The corrected record could be:
example.test. TXT "v=spf1 include:_spf-app.example.test include:_spf-marketing.example.test -all"
That is an illustration, not a record to copy. Your providers, IP ranges, envelope domains, and failure policy determine the real value.
Order matters. RFC 7208, section 4.6.2 evaluates mechanisms from left to right and stops at the first match. A terminal all before later mechanisms makes those later terms unreachable. A redirect modifier also behaves differently from an include, so changing one into the other is not a mechanical cleanup.
While consolidating, count the DNS-triggering terms across nested includes. One SPF record solves the multiple-record error but can still fail the separate 10-term limit. My SPF lookup-limit walkthrough shows how to trace that budget through nested policies.
How do you deploy the correction without guessing?
Lower the TXT record’s TTL in advance if your DNS workflow permits it. Wait for the previous TTL to pass before making the final change, then publish exactly one SPF record. Keep unrelated verification, DKIM, and service TXT records in place.
After the change, query more than one recursive resolver and confirm that each answer contains one selectable SPF record. If the old and new DNS data coexist in caches, results can vary until the previous TTL expires.
Then send a message through every authorized system. In the receiving mailbox, inspect the raw headers and find the trusted receiver’s Authentication-Results field. You want the multiple-record permerror to disappear, but do not stop at “SPF passed.” Record the evaluated envelope domain, SPF result, DKIM result, and DMARC result separately.
The SPF, DKIM, and DMARC guide explains the alignment step. A valid single SPF record does not prove that DMARC passes, that the message is safe, or that inbox placement is guaranteed.
The durable control is simple: give one team or deployment path ownership of each sending domain’s SPF record. Providers can supply the terms they require, but your DNS should have one composed policy, one review trail, and one monitored record.