Bali, Indonesia
Insights

Thinking Two SPF TXT Records Merge, Until dig Shows Why SPF Hits PermError

November 4, 2026
0:00
Many people assume that DNS works like a config file with includes. If your domain already has an SPF record for your hosting server, and a new newsletter or CRM tool asks you to add its own v=spf1 TXT record, it seems reasonable that receivers will read both and combine them. They don't. When a receiving mail server finds more than one SPF record on the same name, it stops evaluating and returns PermError. Mail from your hosting server then fails SPF, and so does mail from the new tool. This mistake is easy to make because nothing visibly breaks when you save the record. The DNS editor accepts it, the new service's setup wizard may even show a green check, and mail keeps flowing. The damage only shows up later as spam placement, DMARC failures in aggregate reports, or bounces from strict receivers. At the support desk this usually arrives as a vague complaint that "email started going to spam," with no obvious link to the DNS change made a few days earlier.
SPF (Sender Policy Framework) lets a domain publish which servers may send mail using that domain in the envelope sender, the address used for bounces (also called the Return-Path or MAIL FROM). A receiver looks up the TXT records on that domain, keeps only the records that begin with v=spf1, and evaluates the result. The SPF specification, RFC 7208, is explicit about this step. Exactly one SPF record is expected. Zero records gives a result of none. Two or more records give permerror, which means "permanent error." The receiver does not guess which record you meant, does not try to merge them, and does not pick the first one it sees. There is a practical reason for this. DNS does not guarantee the order of records in a response, so "use the first one" would give different answers on different queries. Merging is also ambiguous. Each SPF record ends with an all mechanism such as ~all or -all, and two records could end with different policies. There is no safe way to decide which policy wins, so the specification makes the whole thing an error and leaves it to the domain owner to publish one clear record. The word "permanent" matters too. A temperror suggests a DNS timeout that may succeed if retried. A permerror means the published data itself is broken, so retrying gives the same result.
What a receiver does with a PermError depends on the receiver. Some treat it as a hard failure and reject, some count it against the message in spam scoring, and many use it mainly as an input to DMARC. Because of this variation, the failure can look inconsistent: one mailbox provider delivers normally while another sends everything to spam. Under DMARC, SPF only helps if it passes and aligns with the From domain. A PermError is not a pass, so SPF stops contributing to DMARC for every message from that domain. If your mail is also signed with aligned DKIM, DMARC can still pass on DKIM alone, which is why some messages keep arriving while others don't. Mail without aligned DKIM, such as scripts sending through the local MTA or services that sign with their own domain, loses its only path to a DMARC pass. If the domain publishes p=quarantine or p=reject, those messages are now quarantined or rejected. The point to remember is that the duplicate record doesn't just break the new service. It breaks SPF for the whole domain, including the mail that worked before the change.
The quickest check is to query the TXT records for the domain and look for more than one line that starts with v=spf1. Run it against a public resolver, and also against one of the domain's authoritative nameservers so you see what the zone actually contains, not what a resolver has cached.
BASH
# All TXT records as a resolver currently sees them
dig +short TXT example.com

# Find the authoritative nameservers, then ask one directly
dig +short NS example.com
dig +noall +answer TXT example.com @ns1.example.net
The TXT output often contains unrelated records, such as site verification tokens. Those are fine. Only records that start with v=spf1 count as SPF. The key skill is telling two separate records apart from one record that has been split into several strings.
TEXT
# Illustrative output format: two separate SPF records (PermError)
"v=spf1 a mx include:_spf.provider-one.example ~all"
"v=spf1 include:spf.provider-two.example ~all"

# Illustrative output format: ONE record split into two strings (valid)
"v=spf1 a mx include:_spf.provider-one.example " "include:spf.provider-two.example ~all"
In the first case, dig prints two lines, and each line is its own TXT record beginning with v=spf1. That is the PermError case. In the second case, there is one line containing two quoted strings. Each string inside a TXT record is limited to 255 characters, so long SPF values are often split this way, and RFC 7208 tells receivers to join the strings without adding spaces before parsing. That is the only kind of "merging" SPF does, and it happens within a single record, not across records. Because the strings are joined directly, the space between mechanisms has to be inside one of the strings, as shown at the end of the first string above. Also check that you're looking at the right name. SPF is checked against the envelope sender domain, which for some services is a subdomain such as bounce.example.com rather than the root domain. A v=spf1 record on that subdomain doesn't conflict with the one on example.com. A duplicate only matters when both records sit on the same name.
The fix is to keep one v=spf1 record and move the new service's mechanisms into it. Take everything between v=spf1 and the all term from both records, remove duplicates, and end with a single all policy. Then delete the second record.
TEXT
# Before: two records on example.com
v=spf1 a mx include:_spf.provider-one.example ~all
v=spf1 include:spf.provider-two.example ~all

# After: one merged record on example.com
v=spf1 a mx include:_spf.provider-one.example include:spf.provider-two.example ~all
A few details are worth checking while you do this. Place all last and only once. Mechanisms are evaluated left to right, and anything after all is never reached. If the two records ended with different policies, such as ~all (softfail) and -all (fail), choose deliberately rather than keeping whichever happened to be in the record you edited. Moving to -all is a policy change, not just a cleanup. Count DNS lookups. Mechanisms such as include, a, mx, exists, and the redirect modifier each cost DNS queries, and SPF allows a maximum of 10 per evaluation, counting nested includes. Two records that were each fine on their own can go over the limit once merged, which also produces a PermError. If that happens, see Why SPF Suddenly Returns permerror After Adding One More include for how to count and reduce lookups. Check whether your control panel or DNS host manages SPF for you. Some panels create an SPF record automatically and may recreate or overwrite it when mail settings change. If yours does, make the merge through the panel's own SPF or email authentication setting instead of editing the raw TXT record. Otherwise your merged record may be replaced, or a second record may come back. Don't use the deprecated SPF record type. The DNS record type that once existed specifically for SPF is no longer used. Receivers read TXT records, so the single record belongs in TXT.
After saving, query the authoritative nameserver again with the same dig command. You should see exactly one v=spf1 value. Public resolvers may keep returning the old pair until the record's TTL (time to live) expires, so a check against 8.8.8.8 or 1.1.1.1 can lag behind. That's caching, not a failed change. The TTL column in the full dig answer shows how long resolvers may keep the old data. Then send real mail. Send a test message from each source that uses the domain, such as the hosting server, the new service, and any other tool listed in the record, to a mailbox you control at a large provider. Open the original message source and find the Authentication-Results header. You want spf=pass for each sender. If you still see spf=permerror, either the receiver is still using a cached copy or something else is wrong, most often the 10-lookup limit. If SPF passes but DMARC still fails, look at alignment. The envelope sender domain the service uses may not match your From domain, and that is a separate problem from duplicate records. Finally, if the domain publishes DMARC with a reporting address, watch the next few aggregate reports. They show SPF results per sending IP, which confirms the fix at a scale a single test message can't. The habit worth building is simple. Whenever a new service says "add this SPF record," read that as "add these mechanisms to your existing SPF record." Before saving anything, run dig +short TXT on the domain, confirm there is only one v=spf1 line, and count the lookups. That check takes less than a minute, and it is much easier than tracing a domain-wide delivery problem back to a TXT record added days earlier.