See if your domain meets Gmail, Yahoo, and Microsoft sender requirements. Check free →

Email authentication

RFC 9989: What Changed in DMARC and What to Update

By Adam W., founder of DMARCit · Last updated 2026-09

RFC 9989 replaced RFC 7489 as the DMARC specification in May 2026 and put DMARC on the IETF Standards Track. Your v=DMARC1 record keeps working, and nothing in the new spec forces an edit. What changed is underneath it. The pct= tag is gone. The t= testing flag that replaces it is ignored by receivers that haven't upgraded yet. Subdomain policy is now found by walking the DNS instead of consulting the Public Suffix List. And the spec now says in plain words which domains should not publish p=reject.

The short version, for a domain that already has DMARC

  • Delete pct=, unless it's pct=0 on a test rung. That one stays next to t=y for now.
  • Delete ri= and rf=. RFC 9989 has no rules for either.
  • At enforcement, add np=reject so invented subdomains are covered.
  • Before p=reject, make sure DKIM carries every legitimate stream. RFC 9989 makes that a MUST.
  • Check that whatever reads your aggregate reports accepts the RFC 9990 format.

What RFC 9989 is

RFC 7489, published in March 2015, was never an IETF standard. It came in through the Independent Submissions stream with Informational status. RFC 9989 is the IETF DMARC working group's rewrite, published as a Proposed Standard. Its Appendix C summarizes the changes from 7489; this page covers the ones that touch a domain owner's DNS and rollout plan.

The old single document is now three:

  • RFC 9989 is the protocol: policy records, tags, policy discovery, alignment, and the guidance for domain owners and receivers.
  • RFC 9990 defines aggregate reports, the XML that arrives at your rua= address.
  • RFC 9991 defines failure reports, the per-message reports sent to ruf=.

RFC 9989 also obsoletes RFC 9091, the experimental spec for DMARC on public suffix domains. Its np= tag and the public suffix handling moved into the core spec.

The version tag did not change. Records still start with v=DMARC1, and they have to: a record where v= isn't the first tag is ignored entirely (Section 4.7). New tags don't need a new version because receivers are required to ignore tags they don't recognize. That same rule is behind the biggest transition trap on this page.

Tag changes at a glance

TagRFC 7489RFC 9989What to do
pctPercentage of failing mail the policy applies to (0 to 100)Removed, listed as historicDelete it. Exception: pct=0 on a test rung stays, next to t=y, until your reports show receivers honoring t=y.
tDid not existNew. t=y asks receivers to apply one level below your policyUse it for test rungs instead of a percentage ramp.
npOnly in the experimental RFC 9091Core tag: policy for subdomains that do not existAdd np=reject once your registered domain is at enforcement.
psdOnly in the experimental RFC 9091Core tag: y for a public suffix, n to mark an Organizational Domain, u (the default) to let the tree walk decideLeave it off, or set psd=n on your registered domain (see the tree walk section).
riRequested aggregate report intervalRemoved, listed as historicDelete it. RFC 9989 asks receivers to report at least every 24 hours regardless.
rfFailure report formatRemoved, listed as historicDelete it.
pRequiredRecommended. A record with no valid p= but a valid rua= is read as p=none (RFC 7489 said SHOULD, RFC 9989 says MUST)Keep publishing p= explicitly.
rua / ruf size suffix (!10m)Allowed a maximum report size per URIObsolete syntax that report generators ignoreDrop the suffix next time you edit the record.
v, sp, adkim, aspf, rua, ruf, foAs beforeUnchangedNothing.

None of the removed tags breaks a record. A receiver built to RFC 9989 has no rules for them and moves on. They're dead weight, and a pct= value between 1 and 99 is worse than dead weight, which the next section covers.

pct= is gone, and t=y is not a drop-in replacement

pct= let you apply your policy to a slice of failing mail: pct=25 meant a quarter of failures got the policy and the rest got the next level down. Appendix A.6 of RFC 9989 explains why it went away:

Operational experience showed that the pct tag was usually not accurately applied, unless the value specified was either 0 or 100 (the default), and the inaccuracies with other values varied widely from one implementation to another.

pct=0 had also picked up a second job. Some intermediaries and mailbox providers read it as a signal to handle the message differently, usually by rewriting the From header so the relayed copy wouldn't fail DMARC downstream. The working group kept that behavior and gave it a new name: t, for testing. Per the RFC, t=y and t=n are “meant to be analogous in their application by mailbox providers and intermediaries to the pct tag values 0 and 100, respectively.”

What t=y asks for

Section 4.7 defines t=y as a request not to apply your policy as written. The Domain Owner “has an expectation that the policy applied to any failing messages will be one level below the specified policy.” In practice:

  • p=reject; t=y: failing mail is quarantined, not rejected.
  • p=quarantine; t=y: failing mail gets no DMARC action, as if you'd published p=none.
  • p=none; t=y: no effect. There's nothing below none.

It doesn't change reporting, and it's still a request. Section 5.4 leaves final handling to each receiver's local policy, so treat the one-level-down behavior as what conforming receivers should do, and confirm it in your reports.

The transition trap: receivers that haven't upgraded ignore t=

t= wasn't in any published RFC before May 2026. A receiver still running RFC 7489 code has never heard of it, and RFC 7489 told receivers to ignore tags they don't recognize. That receiver reads v=DMARC1; p=reject; t=y as a plain p=reject. The dry run bounces real mail there.

Receivers don't all upgrade on the day an RFC is published. A few months in, assume some of the mailbox providers your recipients use still behave like RFC 7489. A rollout plan that calls p=reject; t=y a safe rehearsal is right about the receivers that implement RFC 9989 and wrong about the rest.

The fix: publish pct=0 and t=y together

Put both tags on the test rung:

v=DMARC1; p=reject; pct=0; t=y; rua=mailto:dmarc-reports@example.com

Each generation of receiver reads the tag it knows:

  • An RFC 7489 receiver reads pct=0. None of the failing mail is selected for the reject policy, and Section 6.6.4 of RFC 7489 says mail not selected under a reject policy SHOULD be treated as quarantine.
  • An RFC 9989 receiver has no rules for pct= and reads t=y: one level below reject, which is also quarantine.

Same outcome at both. The quarantine rung works the same way: p=quarantine; pct=0; t=y gets ordinary local classification at an old receiver (RFC 7489's wording for mail not selected under quarantine) and p=none treatment at a new one. And pct=0 is one of the two values RFC 9989 says receivers applied accurately, so this pairing doesn't lean on the part of pct= that failed.

Validators that flag every pct= as obsolete are right that the tag is historic, and they miss this case: on a test rung, pct=0 still does work at every receiver that hasn't upgraded. When the rung is done, drop both tags together.

How to tell which receivers honored it

RFC 9990 reports make this visible. The report schema says a receiver MUST give a reason in policy_evaluated whenever the policy it applied to failing mail differs from the one you published, and the reason type for this case is policy_test_mode. The policy_published block also echoes the flag back as <testing>y</testing>.

Reports in the RFC 7489 format, with no testing element at all, come from receivers you should assume ignored t=y. Once the receivers that carry most of your volume report policy_test_mode, pct=0 has stopped doing any work and you can drop it.

If your record still has pct=50

A leftover mid-ramp value is the case that hurts. An RFC 9989 receiver ignores pct= and applies the policy to all failing mail. An RFC 7489 receiver applies it to half. You are already at full enforcement for part of your recipients, whether you meant to be or not. Pick one on purpose: commit to the full policy and delete pct=, or step back to a test rung with pct=0; t=y and climb again. The staged enforcement rollout guide walks through the rungs.

The DNS tree walk replaces the Public Suffix List

DMARC needs the Organizational Domain for two jobs: finding a policy when the From domain has no record of its own, and deciding whether example.com and mail.example.com count as aligned under relaxed mode. RFC 7489 got it from a public suffix list, didn't say which list, and didn't say how often to refresh it. RFC 9989 drops the list and asks the DNS instead (Section 4.10).

The receiver first queries _dmarc at the From domain itself. If there's no record, it walks up one label at a time toward the root. The walk is capped at eight queries: a name with eight or more labels is shortened to seven before the walk continues, so an attacker can't make receivers chase a hundred-label From domain. To pick the Organizational Domain from the records it found, the receiver goes from the longest name to the shortest:

  1. A record with psd=n marks that domain as the Organizational Domain. Stop.
  2. A record with psd=y (anywhere other than where the walk started) marks a public suffix, so the Organizational Domain is the name one label below it. Stop.
  3. Otherwise, the record with the fewest labels wins.

For a typical setup nothing changes. Mail from news.example.com, with a record only at _dmarc.example.com, queries _dmarc.news.example.com, then _dmarc.example.com, then _dmarc.com, finds one record, and lands on example.com: the same answer the Public Suffix List gave. The differences show up at the edges.

psd=n on a subtree can break relaxed alignment

The tree walk lets a team that runs its own slice of DNS declare it an Organizational Domain. Publish a record at _dmarc.dept.example.com with psd=n, and dept.example.com gets its own policy and its own report address. It also becomes its own island for relaxed alignment. Mail From: someone@dept.example.com signed with d=example.com used to align, because the Public Suffix List put both names under example.com. Under the tree walk, the From domain's Organizational Domain is dept.example.com, the signature's is example.com, and relaxed alignment fails. If you set psd=n on a subtree, sign that subtree's mail with a key under the subtree.

A public suffix above you that publishes DMARC

The fewest-labels rule has a failure mode the RFC calls out in its security considerations. If a public suffix above your domain publishes a DMARC record without psd=y, a receiver can pick the suffix as your Organizational Domain, and relaxed alignment starts treating unrelated registrants under that suffix as one organization. The RFC's suggested guard is adding psd=n to the record on your registered domain, which pins the boundary where it belongs. It's optional, and receivers on RFC 7489 ignore it. On a registered domain it states what is already true, so the cost is one more tag.

Long names and mixed receivers

Two smaller edges. A From domain with more than eight labels skips some of its ancestors during the walk, so a policy published partway up may never be read; the RFC's fix is a record at the exact From domain. And while old and new receivers coexist, a list-based receiver and a tree-walking one can reach different answers for unusual layouts. RFC 9989's own advice covers both: publish explicit records for every domain you send from, and use strict alignment where you can. RFC 9990 reports tell you which method each receiver used, in a discovery_method field that reads psl or treewalk.

A record for every domain you send from

RFC 9989's list of what full DMARC participation means for a domain owner includes a record for each Author Domain (the domain in the From header) and for its Organizational Domain. If marketing mail goes out From: news.example.com, that means _dmarc.news.example.com, not just the parent. The trade-off is maintenance. A subdomain's own record inherits nothing: it needs its own rua=, and its p= wins over the parent's sp=. The failure to watch for is tightening sp=reject on the parent while a forgotten subdomain record still says p=none.

np= covers subdomains that don't exist

Spoofers don't limit themselves to names you use. They invent them: billing.example.com, secure-login.example.com, names that have never existed in your DNS. Under RFC 7489 those fell under sp=, and plenty of domains keep sp= softer than p= because some real subdomain sender isn't aligned yet. np= splits the two cases. It applies only to subdomains that don't exist, and when it's absent receivers fall back to sp=, then to p=.

v=DMARC1; p=reject; sp=quarantine; np=reject; rua=mailto:dmarc-reports@example.com

Here the registered domain rejects, real subdomains get quarantine while their senders are cleaned up, and invented subdomains get rejected outright. Three details decide whether np= does anything for you:

  • “Doesn't exist” means NXDOMAIN, per RFC 8020. A name with any record at all exists, so wildcard DNS makes every subdomain exist and np= never applies. If you run a wildcard, sp= is still your subdomain policy.
  • It's read from the Organizational Domain's record. Putting np= on an ordinary subdomain's record does nothing for that subdomain's children.
  • Older receivers skip it. A receiver that implements neither RFC 9989 nor the experimental RFC 9091 ignores np= and uses sp= or p=.

The p=reject guidance got explicit

RFC 9989 spends Section 7.4 on what p=reject does to mail that takes an indirect path, with normative language aimed at both sides of the connection.

For domain owners

It is therefore critical that domains that host users who might post messages to mailing lists SHOULD NOT publish Domain Owner Assessment Policies of “p=reject”.

Domains that publish p=reject anyway are asked to spend at least a month at p=none and an equally long period at p=quarantine first, comparing dispositions, and then either keep their users off Internet mailing lists or warn them that list participation may break. The same section adds a hard requirement for any p=reject domain: it MUST NOT rely solely on SPF for a DMARC pass, and it MUST apply valid DKIM signatures. Forwarders and aliases usually break SPF unless they rewrite the envelope sender, while DKIM signatures generally survive the relay.

For receivers

It is therefore critical that Mail Receivers MUST NOT reject incoming messages solely on the basis of a “p=reject” policy by the sending domain.

Without other evidence, such as sending history or content filtering, a receiver is told to treat failing mail as if the policy were p=quarantine. The RFC is also candid that practice lags the text: it notes that few receivers apply mitigations for indirect mail flows, and that mail relayed through mailing lists with an unmodified From header is still frequently rejected.

What this means for your rollout

p=reject is now a strong request rather than a guaranteed bounce, and the spec itself steers domains whose people post to mailing lists away from it. The clean place for p=reject is a sending subdomain nobody posts to lists from: transactional mail, marketing, notifications. Those can go to reject on their own records while the domain your staff use holds at quarantine. If you're deciding between the two, the quarantine versus reject guide goes deeper.

Aggregate reports under RFC 9990

The rua= reports your tools parse are now defined in RFC 9990. The structure is familiar, with a few changes worth knowing when you read them:

  • A new XML namespace, urn:ietf:params:xml:ns:dmarc-2.0, on the feedback root element.
  • policy_published gains np, testing (your t value), and discovery_method (psl or treewalk). pct is not part of the new schema.
  • Override reasons in policy_evaluated are local_policy, mailing_list, trusted_forwarder, other, and the new policy_test_mode. The RFC 7489-era forwarded and sampled_out values are gone from the schema.
  • A reported DKIM result now has to include the selector, which makes it easier to tell which key failed.
  • Receivers SHOULD send reports at least every 24 hours, and a reporting period typically covers one UTC day.

For a while your inbox gets both formats, since receivers upgrade on their own schedules. Whatever parses your reports needs to accept both. If you plan to use t=y rungs, it also needs to surface testing and policy_test_mode, because those two fields are how you know the rung is behaving.

Failure reports moved to RFC 9991. ruf= and fo= still exist, but sending failure reports is optional for receivers, and RFC 9989 notes that privacy concerns lead many to redact them heavily or not send them at all. Don't build a rollout gate on ruf=.

What to update, in order

  1. Pull your current record. The free DMARC checker shows the record you're actually publishing.
  2. Deal with pct=. Absent or 100: delete it. Between 1 and 99: you're already at full policy at upgraded receivers, so commit to it or step back to pct=0; t=y. Exactly 0: add t=y next to it and keep both until your reports show policy_test_mode from the receivers that matter.
  3. Delete ri= and rf=, and drop any ! size suffix from rua= and ruf= addresses.
  4. Add np=reject if your registered domain is at quarantine or reject, after confirming you don't run wildcard DNS.
  5. List every domain you send From. Give each one its own record with its own rua=, or confirm on purpose that the parent's sp= covers it.
  6. Before any p=reject, confirm DKIM. Every legitimate stream should pass DKIM with an aligned d= domain. SPF-only streams fail at the first forwarder.
  7. Decide where p=reject belongs. Sending subdomains, yes. The domain your staff use on mailing lists: RFC 9989 says SHOULD NOT, unless you accept the breakage and tell your users.
  8. Optionally add psd=n to the record on your registered domain.
  9. Check your report tooling against RFC 9990 reports, including testing and policy_test_mode.
  10. Rewrite any runbook that still ramps pct=25, 50, 100. The staged rollout guide has the t= version, with the pct=0 pairing for the transition.

What didn't change

Most of what you already know still holds:

  • The policy lives in a TXT record at _dmarc.<domain> and starts with v=DMARC1.
  • A message passes DMARC when SPF or DKIM passes with a domain aligned to the From domain. One aligned pass is enough.
  • Relaxed alignment still means the same Organizational Domain; strict still means identical domains. Relaxed is still the default.
  • p=, sp=, adkim=, aspf=, rua=, ruf=, and fo= keep their syntax and meaning.
  • The bulk-sender rules at Gmail, Yahoo, and Microsoft are provider policies, and RFC 9989 doesn't change them.

RFC 9989 FAQ

Do I need to change my DMARC record because of RFC 9989?

No edit is required. RFC 9989 kept the v=DMARC1 version tag, and a record that parsed under RFC 7489 still parses. A few edits are worth making: delete pct= unless you are using pct=0 on a test rung, delete ri= and rf=, and once your domain is at enforcement add np= to cover subdomains that do not exist.

Is the DMARC pct tag still supported?

RFC 9989 removed pct= and lists it as historic in the IANA DMARC tag registry. Receivers that implement RFC 9989 have no rules for it and apply your policy to every failing message. Receivers still running RFC 7489 code keep honoring it. The t=y testing flag replaces the one pct value that worked reliably, pct=0.

What does t=y do in a DMARC record?

t=y marks your policy as under test. Receivers that implement RFC 9989 are expected to apply one level below what you published: p=reject is handled as quarantine and p=quarantine as none. It has no effect at p=none and does not change reporting. Receivers that predate RFC 9989 ignore the tag, so during the transition pair it with pct=0 on test rungs.

What is the DMARC DNS tree walk?

It is how RFC 9989 receivers find a policy and the Organizational Domain without a Public Suffix List. The receiver queries _dmarc at the From domain, then walks up one label at a time, capped at eight queries. Unless a psd= tag says otherwise, the record with the fewest labels marks the Organizational Domain, which for most senders is the same registered domain the Public Suffix List gave.

Is there a DMARC version 2?

No. RFC 9989 keeps v=DMARC1. New tags do not need a new version because receivers must ignore tags they do not recognize. The aggregate report XML did get a new namespace in RFC 9990, but the DNS record version did not change.

See what your record says today

The free checker pulls the DMARC record you're actually publishing, so you can see whether pct=, ri=, or rf= are still in it. To see which senders are behind your reports, paste one into the free report parser; it runs in your browser.

Related reading

The specifications