← Blog
RFC 9989 DMARCbis: What Actually Changed for Your Domain

· Updated

By DMARCdrift Team

RFC 9989 DMARCbis: What Actually Changed for Your Domain

8 min readdmarcrfcemail-authentication

RFC 9989 (DMARCbis) published on May 19, 2026 and your existing v=DMARC1 record still works without modification. The three new RFCs (9989 core, 9990 aggregate reporting, 9991 failure reporting) replace the original RFC 7489 from 2015. RFC 9989 obsoletes RFC 7489 and moves DMARC from Informational to Standards Track, and reporting now lives in its own two documents. Two changes are worth acting on now: pct= is removed, with a new t= testing tag taking over its test-mode role, and np= lets you set an explicit policy for non-existent subdomains.

What didn't change

The core authentication mechanism is identical. v=DMARC1 remains valid. p=none, p=quarantine, and p=reject are the same three values. rua= and ruf= addresses are unchanged. sp=, adkim=, aspf=, and fo= are still there, though the fo= grammar is stricter (fo=0:1 is no longer valid).

One clarification on p=reject is worth knowing. RFC 9989 §7.4 says receivers MUST NOT reject mail solely because of p=reject, and that domains publishing p=reject MUST sign their mail with valid DKIM rather than rely on SPF alone. The same section says domains whose users post to mailing lists SHOULD NOT publish p=reject; DMARCbis and mailing lists covers who that applies to.

Receiving servers will continue to process your existing records without modification. The IETF was deliberate about backward compatibility. This revision codifies a decade of implementation experience without breaking deployed infrastructure.

If you're at p=none still working toward full alignment, nothing about DMARCbis changes your immediate priorities. Focus on alignment first.

What changed: pct= is removed

pct= allowed you to apply your DMARC policy to only a percentage of failing messages. pct=50 meant: apply your policy to 50% of failures, and handle the other 50% one level softer (quarantine instead of reject, or normal delivery instead of quarantine). It was designed as a gradual rollout mechanism.

RFC 9989 removes pct= from the spec.

The reason, per Appendix A.6, is that pct= "was usually not accurately applied, unless the value specified was either 0 or 100", and the inaccuracies varied widely between implementations. As a sampling mechanism for staged enforcement, it was unreliable in practice. You couldn't depend on it to behave the way you expected.

The right approach to staged rollout is to use policy levels themselves: start at p=none, move to p=quarantine once your alignment rate is solid, then move to p=reject. Each step is unambiguous and universally implemented. If you want a test step in between, use the new t= tag (below).

RFC 9989 lists pct as "historic" in the IANA DMARC tag registry and defines no processing for it. Receivers that implement RFC 9989 ignore it, while receivers still on RFC 7489 may keep applying it. That's the key shift: if you have pct=50 or pct=25 in your record, you cannot assume it's doing what you think it's doing. Some receivers may now enforce your full policy on 100% of failing messages regardless.

pct= isn't the only tag gone. rf= (failure report format) and ri= (aggregate report interval) were removed too (Appendix C.5.2). Delete them from your record. In our September 2026 scan, 828 of 1,786 domains with a DMARC record (46.4%) still carried at least one of the three; see how many domains still publish pct, rf, and ri for the breakdown.

If you have pct= at anything below 100 and you're at p=quarantine or p=reject, remove it. Figure out what policy level you actually want and set it explicitly. If you're not ready for full enforcement, drop back to a lower policy level rather than relying on sampling.

What changed: np= is new

RFC 9989 brings np=, the non-existent subdomain policy tag, into the core spec. It was first defined in RFC 9091, the experimental public suffix DMARC spec that RFC 9989 obsoletes. It's the new tag most domain owners should act on.

Previously, sp= covered all subdomains that didn't have their own DMARC record. "All subdomains" turned out to be ambiguous: does it include subdomains that don't exist in DNS at all?

np= resolves this. It applies specifically to subdomains that return NXDOMAIN, meaning they don't exist (§3.2.13). sp= continues to apply to subdomains that exist in DNS but don't have their own DMARC policy.

Without an explicit np= tag, non-existent subdomains fall back to the organizational domain's sp= policy, or to p= if there is no sp= (§4.7). For domains at p=reject with no softer sp=, that means NXDOMAIN subdomains already get reject behavior, but the intent isn't explicit in the record. The np= tag explainer walks through how np=, sp=, and p= take precedence.

Adding np=reject makes your intent unambiguous and ensures consistent handling across receivers that implement DMARCbis. For most domains at p=reject, the update is straightforward:

v=DMARC1; p=reject; sp=reject; np=reject; rua=mailto:...

This matters if you have an attacker trying to spoof definitely-not-real.yourdomain.com, a subdomain that doesn't exist. With np=reject explicit, there's no ambiguity about what receivers should do.

What changed: the t= testing tag

RFC 9989 adds t=, a testing-mode tag (§4.7). t=y does not switch enforcement off. It asks receivers to apply your policy one level below what you published: p=reject is handled as quarantine, and p=quarantine is handled as none. It has no effect at p=none, and it doesn't change reporting. The same step-down applies to sp= and np= when either is the policy in effect. The DMARC t=y testing mode guide covers the tag in more depth.

t=y replaces the useful part of pct=0 (Appendix A.6). Some intermediaries and mailbox providers treated pct=0 as a signal to rewrite the From header, and t=y is meant to carry that same signal. RFC 9989 calls the two "analogous" but doesn't tell receivers to treat pct=0 as t=y. Receivers still on RFC 7489 ignore t=, and RFC 9989 receivers ignore pct=, so during the transition publish both:

v=DMARC1; p=reject; t=y; pct=0; rua=mailto:...

Both kinds of receiver then apply quarantine-level handling. When you're ready to enforce, remove both tags. The pct= migration guide walks through a full staged sequence.

How quickly receivers adopt t= isn't known yet. RFC 9990 aggregate reports include a testing field that echoes the t value the receiver saw in your record.

What changed under the hood: DNS Tree Walk

RFC 9989 replaces the Public Suffix List (PSL) with a DNS Tree Walk for finding the organizational domain (§4.10, Appendix C.3). Instead of consulting a third-party list, receivers query _dmarc records up the DNS hierarchy from the From domain. The walk is capped at eight DNS queries: a domain with more than eight labels is shortened before the walk continues.

For most domain owners this is invisible. Two cases where it isn't:

  • Deeply nested sending domains. If you send from a From domain with more than eight labels and want a specific policy for it, RFC 9989 §5.1.8 says you MUST publish a DMARC record at that exact name, because the shortened walk can skip the levels in between.
  • Mixed receivers during the transition. A receiver still using RFC 7489 and a PSL can reach a different organizational domain than one using the tree walk. Appendix C.3 says this "is entirely avoided by the use of strict alignment and publishing explicit DMARC Policy Records for all Author Domains."

The tree walk comes with a new psd= tag (y, n, or u, default u). Public suffix operators, such as TLD registries, publish psd=y to mark a domain as a public suffix. psd=n declares that a domain is the organizational domain for itself and its subdomains. Most domain owners should leave psd= out. The tree walk and psd= guide explains how the walk finds your organizational domain and who should set psd=.

What to do now

If you have pct= between 1 and 99 at p=quarantine or p=reject: Remove it. Decide what policy level you actually want and set it explicitly. pct=50 is not a safe middle ground anymore.

If you use pct=0 as a test mode: Add t=y next to it. RFC 9989 receivers ignore pct=0 on its own and apply your full policy.

If you have rf= or ri=: Delete them. Both were removed in RFC 9989.

If you're at p=reject: Consider adding np=reject to make your non-existent subdomain policy explicit. It's a one-line change that closes an ambiguity.

If you're at p=none: No changes needed from DMARCbis. Your immediate job is still getting alignment above 98% before you tighten policy.

If you have sp= set: Review whether you want np= to match. If sp=reject, you almost certainly want np=reject too.

DMARCdrift's domain health checker flags pct= as a tag removed in RFC 9989 and suggests adding np= for domains at p=quarantine or p=reject. If you want to see how your current record stacks up against the DMARCbis spec, run a check on your domain.

The short version: DMARCbis doesn't break anything. But if you've been using pct= as a deployment safety net, that net has a hole in it now. Time to remove it and own the policy level you're actually at.

Updated September 28, 2026: corrected the publication date, the t= testing-tag description, and the np= fallback, and added the psd= tag, the removed rf= and ri= tags, and the tree walk limits.