Some DMARC mistakes fail loudly. Most do not.
The dangerous ones are the records that look published in DNS, maybe even show up in a quick TXT lookup, but are unusable enough that receivers treat them as if no real enforcement policy exists.
That is how teams end up saying "we already published p=reject" while spoofed mail still gets through.
If the short version is enough, check these first:
_dmarc.example.com hostv=DMARC1 first, then a valid p= tagPer RFC 7489 Section 6.1, DMARC records live at the _dmarc subdomain.
For example.com, the record belongs here:
_dmarc.example.com TXT "v=DMARC1; p=reject; rua=mailto:dmarc@example.com"Not here:
example.com TXT "v=DMARC1; p=reject; rua=mailto:dmarc@example.com"That wrong-host mistake is more common than it should be, especially in DNS panels that hide the full record name or auto-append the zone name in slightly confusing ways.
If the record is published at the zone apex instead of _dmarc, receivers doing a DMARC lookup will not find a DMARC policy for the domain. It is that simple.
If a DMARC checker says "no DMARC record found" but DNS clearly returns a TXT record somewhere, check the owner name first.
DMARC is not like a list you can extend by adding another TXT row later.
For a domain, there should be one DMARC policy record at _dmarc.example.com. If multiple DMARC records are returned for that host, receivers can treat the result as invalid. In practice, that usually means your intended policy is not reliably applied.
This often happens after migrations:
p=none record is left in placep=quarantine or p=reject record gets added beside itBroken example:
_dmarc.example.com TXT "v=DMARC1; p=none; rua=mailto:dmarc@example.com"
_dmarc.example.com TXT "v=DMARC1; p=reject; rua=mailto:dmarc@example.com"Correct approach:
_dmarc.example.com TXT "v=DMARC1; p=reject; rua=mailto:dmarc@example.com"Do not confuse this with DNS string splitting inside a single TXT record. A provider can store one long TXT value as multiple quoted strings that DNS concatenates. That is normal. Two separate DMARC policy records are the problem.
The base record format is stricter than many admins expect.
Per RFC 7489 Section 6.3, v and p are required in a policy record, and v=DMARC1 must appear first.
These are valid starting points:
v=DMARC1; p=none
v=DMARC1; p=none; rua=mailto:dmarc@example.com
v=DMARC1; p=reject; rua=mailto:dmarc@example.comThese are not:
p=reject; rua=mailto:dmarc@example.com
v=DMARC1; rua=mailto:dmarc@example.com
v=DMARC1; p=
v=DMARC1; p=monitorTwo practical reminders:
rua is useful, but it is optionalp is not optional, even when the goal is "monitoring only"If the domain is still in discovery mode, the safe minimal record is:
v=DMARC1; p=none; rua=mailto:dmarc@example.comIf you want a fuller explanation of what each tag does, DMARC record tag cookbook goes tag by tag.
This is the category that causes the most "but it looks right" outages.
DMARC uses a tag-value format, and small syntax errors matter:
p=monitormailto: URIs in ruav=DMARC1 somewhere other than firstExamples of broken records:
v=DMARC1, p=reject, rua=mailto:dmarc@example.com
v=DMARC1; policy=reject; rua=mailto:dmarc@example.com
rua=mailto:dmarc@example.com; v=DMARC1; p=reject
v=DMARC1; p=reject; rua=dmarc@example.comExamples of records that are boring but correct:
v=DMARC1; p=none; rua=mailto:dmarc@example.com
v=DMARC1; p=quarantine; rua=mailto:dmarc@example.com; pct=25
v=DMARC1; p=reject; rua=mailto:dmarc@example.com; adkim=s; aspf=sRFC 7489 explicitly says unknown tags must be ignored, but syntax errors in the actual record can cause the usable parts to be ignored or defaulted in ways you did not intend. That is why it is safer to start simple and add complexity only after the base record validates cleanly.
Seeing some TXT output in DNS is not the same as publishing a valid DMARC policy.
A basic DNS lookup can confirm that a TXT record exists. It does not confirm that:
_dmarc.<domain>That is why a "record exists" result from a generic DNS tool is only the first check, not the last one.
Use both kinds of validation:
_dmarc.example.com return the expected TXT value?This is less about RFC syntax and more about rollout discipline.
Teams sometimes jump straight to this:
v=DMARC1; p=rejectThat record is syntactically valid. But if it is published on the wrong host, duplicated, or copied with a tiny syntax error, the team may believe enforcement is active when it is not.
The safer sequence is:
p=none record at _dmarc.<domain>p=quarantine, then p=reject when the domain is actually readyThat rollout pattern is covered in more detail in A sane DMARC setup process for busy domains and DMARC policy modes explained.
Before assuming DMARC enforcement is live, verify all of this:
_dmarc.example.comv=DMARC1; p=...p is one of none, quarantine, or rejectrua, adkim, aspf, pct, and sp are spelled correctlyIf spoofed mail is still appearing after that, the problem usually shifts from "bad DMARC DNS syntax" to "receiver handling, non-DMARC abuse, or legitimate sources that are still misaligned". For that next layer, DMARC troubleshooting with Authentication-Results headers is the better place to look.
The most common DMARC record mistakes are boring: wrong hostname, duplicate records, missing p, and tiny syntax errors.
They are also exactly the mistakes that make teams think enforcement exists when receivers are effectively ignoring the policy.
So the safest DMARC record is not the most advanced one. It is the one valid record, at the correct _dmarc host, with a clean v=DMARC1; p=... core that every receiver can parse the same way.