Skip to content

SPF syntax errors: what breaks a record and how to fix each one

Breakage - syntax errors trigger permerror before any mechanism is evaluated

Critical / Warning
Breakage / Hygiene
Email Authentication
SPF

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.0 and ip4:1.2.3.4/33 both break the record.
  • For IPv6, the prefix must be 0 to 128.
  • Missing or malformed domain arguments after include, a, mx, or exists fall in the same category.
  • Macro syntax: a % character not followed by {, %, -, or _ is a syntax error. So exists:%(ir).example.org with 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. The all mechanism always matches and evaluation stops at the first match. The spec states that mechanisms listed after allMUST be ignored. So in v=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.
  • redirect when an all is present - Warning. The spec requires any redirect modifier to be ignored if there is an all mechanism anywhere in the record, regardless of relative ordering. Not just when all comes first - anywhere. So v=spf1 include:x -all redirect=_spf.example.com silently 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:

"v=spf1 include:a.example.com" " include:b.example.com" ← correct
"v=spf1 include:a.example.com" "include:b.example.com" ← broken

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

  1. Start from the specific term named in the finding.
  2. For an unknown mechanism, check for a typo first, then check whether the term was meant to be a modifier with an =.
  3. For a malformed IP or prefix, fix the literal against the sender's documented values. Verify the CIDR length is inside the legal range.
  4. If more than one SPF record exists at the name, merge them into a single v=spf1 record 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.
  5. Remove duplicate mechanisms, and remove any duplicate redirect or exp.
  6. Move real mechanisms that sit after all in front of it. If you find a redirect and an all in the same record, decide which one you meant and delete the other.
  7. Check for split-string damage if the record is long.
  8. 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).