SPF syntax errors: what breaks a record and how to fix each one
Breakage - syntax errors trigger permerror before any mechanism is evaluated
Critical
Your record parses at the surface but contains a problem that changes how receivers evaluate it. A record can start with v=spf1 and end in -all while still failing at every receiver because of a syntax error in the middle.
Record selection: before syntax even starts
Two rules run before any mechanism is looked at.
The version string must match exactly. Receivers fetch the TXT records at your name and discard everything that does not begin with exactly v=spf1, terminated by a space or the end of the record. A subtle typo in the version does not produce an error, it produces silence: your record is discarded, no SPF record is found, and the result is none.
Only one record may survive selection. If more than one record remains after filtering, the result is permerror. Receivers do not merge them, do not pick the longer one, and do not prefer the newer one. Both are discarded and the check fails. This is one of the most common breakages in the wild, arising from adding a new include as a separate TXT record instead of into the existing one.
Mechanisms and modifiers are not the same thing
This distinction decides whether an unrecognized term is fatal or harmless, and it is the single most misunderstood part of SPF syntax.
Modifiers always have an = immediately after the name. Terms that contain none of =,:, or / are mechanisms. Most mechanisms allow a : or / after the name.
Unknown mechanism → Critical (permerror). bogus:example.com is a mechanism (has a colon) and is not one of the nine defined mechanisms. That is a syntax error - permerror for the whole record.
Unknown modifier → ignored safely. bogus=example.com is a modifier and the spec requires receivers to ignore unrecognized modifiers. Vendor-specific name=value additions do no harm.
The nine mechanisms are all, include, a, mx, ptr,ip4, ip6, and exists, plus the qualifiers in front of them. The two defined modifiers are redirect and exp. Names are case-insensitive.
Malformed arguments
Invalid values inside an otherwise correct mechanism are syntax errors, and therefore Critical.
- For IPv4, each of the four parts must be 0 to 255, and the CIDR length must be 0 to 32. So
ip4:999.999.0.0andip4:1.2.3.4/33both break the record. - For IPv6, the prefix must be 0 to 128.
- Missing or malformed domain arguments after
include,a,mx, orexistsfall in the same category. - Macro syntax: a
%character not followed by{,%,-, or_is a syntax error. Soexists:%(ir).example.orgwith parentheses instead of braces fails the whole record.
Duplicate modifiers
redirect and exp must each appear at most once. Two of either producespermerror - Critical. Duplicate mechanisms are different: listing include:example.com twice is not an error, but it is dead weight and wastes a DNS lookup. DNSAudit treats duplicate mechanisms as a Warning.
Dead terms: things that are silently ignored
These do not break the record. They mean part of it is doing nothing, which is sometimes worse because the record appears to say something it does not.
- Mechanisms after
all- Warning. Theallmechanism always matches and evaluation stops at the first match. The spec states that mechanisms listed afterallMUST be ignored. So inv=spf1 -all include:example.com, that include is dead. Someone added a sender to the end of the record without noticing the terminator was already there. redirectwhen anallis present - Warning. The spec requires anyredirectmodifier to be ignored if there is anallmechanism anywhere in the record, regardless of relative ordering. Not just whenallcomes first - anywhere. Sov=spf1 include:x -all redirect=_spf.example.comsilently drops the redirect.
The split-string trap
A single TXT record can be made of multiple character strings, because one string cannot exceed 255 octets. The spec says the strings are concatenated with no spaces added. If you split a long record at a word boundary and let your DNS provider drop the space:
The second version concatenates into ...a.example.cominclude:b.example.com, which is a mangled term. The record looks right in a zone file editor and is broken in evaluation.
How to fix it
- Start from the specific term named in the finding.
- For an unknown mechanism, check for a typo first, then check whether the term was meant to be a modifier with an
=. - For a malformed IP or prefix, fix the literal against the sender's documented values. Verify the CIDR length is inside the legal range.
- If more than one SPF record exists at the name, merge them into a single
v=spf1record and delete the extras. Take the includes and IP mechanisms from each, put them in one record, and finish with a single terminator. Then count your lookups, because merging two records often lands you near the limit. - Remove duplicate mechanisms, and remove any duplicate
redirectorexp. - Move real mechanisms that sit after
allin front of it. If you find aredirectand anallin the same record, decide which one you meant and delete the other. - Check for split-string damage if the record is long.
- Re-scan to confirm the record parses cleanly, then check the lookup count and the terminator separately.
How this is rated
Severity splits by outcome. Cases that produce permerror rate Critical: an unknown mechanism, a malformed IP or prefix, more than one SPF record at the name, or a duplicate redirect or exp. Multiple records is the one you will meet most often - it earns Critical because both records are discarded rather than merged, leaving the domain with no working SPF at all.
Cases that are dead weight rate Warning: mechanisms after all, aredirect ignored because an all is present, and duplicate mechanisms. None of those break evaluation, but each means the record is not doing what it appears to.
Reference: RFC 7208 section 3.3 (multiple strings), section 3.4 (record size), section 4.5 (selecting records), section 4.6 (record evaluation and syntax), section 4.6.1 (term ABNF), section 5.1 (all), section 6 (modifiers), section 6.1 (redirect).
