DMARCbis Is Now RFC 9989: What Actually Changed, and the t= Trap Everyone Is Getting Wrong

Updated Takes effect: Published May 2026

“DMARCbis” was the IETF working name for the rewrite of the DMARC specification. It stopped being a draft in May 2026, when it was published as three RFCs, so strictly speaking there is no DMARCbis any more. There is just DMARC, and it now works differently under the hood than the version almost every guide on the internet still describes.

If you last read the spec as RFC 7489, you learned a version of the protocol that is now obsolete. The rewrite kept the record you already publish working, but it changed how receivers decide what your domain even is, retired several tags outright, and added the single most useful anti-spoofing control the standard has ever had. Everything below is checked against RFC 9989 itself rather than the second-hand summaries, some of which are already getting the important part wrong.

One practical thread runs through all of it: a new discovery_method field in the reports (RFC 9990) lets you check, from your own data, which mailbox providers have actually switched to the new rules - so you do not have to take any vendor’s word for it. More on that below.

Three documents, not one

The monolithic RFC 7489 was split into a core protocol plus two reporting specs, all published together in May 2026. In the process DMARC was promoted from an Informational document to a Proposed Standard for the first time in its history.

  • RFC 9989 is the core protocol. Its formal Obsoletes header names both RFC 7489 (the original 2015 spec) and RFC 9091 (the PSD experiment), folding the public-suffix-domain work into the core.
  • RFC 9990 covers aggregate reporting, the rua side: the daily XML digests receivers send back, and their schema.
  • RFC 9991 covers failure reporting, the ruf side: the near-real-time, per-message failure reports.

The big change: the Public Suffix List is gone

This is the part that matters most and gets the least coverage.

DMARC passes when SPF or DKIM aligns with your From address, and by default “aligns” does not mean an exact match - it means the two domains belong to the same organization. news.acme.com and mail.acme.com align because both roll up to acme.com. So every alignment check depends on one question: where is the boundary between “a company’s own domain” and “the public registry above it”?

The old approach (RFC 7489) was a shared list. Receivers consulted the Public Suffix List, a community-maintained text file of registry endings (.com, .co.uk, and thousands more). The organizational domain was “the public suffix, plus one label to its left.” The list also has a “private” section for hosting platforms that hand their customers a registrable name each - github.io is the classic example, where alice.github.io and bob.github.io are meant to count as separate organizations. Nobody was required to use the same copy of the list, so different receivers carried different, differently-aged versions - and could disagree.

The new approach (RFC 9989) is to ask DNS. Instead of a file, the receiver does a tree walk - it queries _dmarc up the domain’s labels and lets DNS itself reveal the boundary (capped at eight lookups, in a fixed order of preference: the Author Domain first, then the Organizational Domain, then the Public Suffix Domain). A platform that wants each customer treated as its own organization simply publishes psd=y on its own _dmarc record, and the tree walk picks that boundary up live from DNS - no waiting for a shared list to be updated.

Here is the walk in practice, for a message from [email protected]:

_dmarc.sub.mail.example.com   ->  no record
_dmarc.mail.example.com       ->  no record
_dmarc.example.com            ->  record found (read psd= to confirm the boundary)
Organizational Domain = example.com

Why this can flip a pass into a fail. Take a hosting platform whose domain is cloudplatform.io. It is not a TLD - the TLD is .io - it is an ordinary domain that acts as a registry boundary, giving each customer a registrable name of their own such as acme.cloudplatform.io (exactly like username.github.io). Your client owns the name acme.cloudplatform.io, and the platform signs its outbound mail with a shared key, d=mailer.cloudplatform.io.

  • If a tool does not treat cloudplatform.io as a boundary - an old list that never added it (the Public Suffix List does carry hosting domains like github.io in its private section, but it only helps if your copy of the list includes that entry) - then both acme.cloudplatform.io and mailer.cloudplatform.io look like parts of one domain, cloudplatform.io. Same organization, so they align and DMARC passes.
  • If cloudplatform.io is recognized as a boundary - because it now publishes psd=y, which the tree walk honours - then each customer name is its own organization. Now acme.cloudplatform.io and mailer.cloudplatform.io are different organizations, so they do not align and DMARC fails.

Same message, opposite verdicts, purely because the two methods drew the company boundary in different places. And the finer boundary is the correct one: it is exactly what stops another customer, globex.cloudplatform.io, from signing mail that aligns with your client’s acme.cloudplatform.io.

Practically, alignment is now something you can reason about from DNS alone, rather than depending on which snapshot of a text file a given receiver happens to hold - provided the receiver has actually implemented the tree walk. (More on who has, below.)

The tag changes

Some tags are gone, two are new, and one common one changed meaning in a way secondary sources are already botching.

Tag Status What to know
pct Removed Percentage sampling. Removed outright, not deprecated (RFC 9989 Appendix A.6).
rf, ri Removed Report format and interval. Gone.
t New Replaces pct, but as a step-down, not an off switch. See below.
np New Policy for non-existent subdomains. The sharpest anti-spoofing control in the record.
psd New Declares public-suffix status: y / n / u (default). Most owners never touch it.
sp Narrowed Now governs only subdomains that actually exist. Non-existent ones fall to np.
v=DMARC1 Unchanged No version bump. This is precisely why nothing breaks.

The t= tag: a test drive for your policy

Think of your DMARC policy as a three-position dial:

  • p=none - watch only. Nothing is blocked; you just collect reports.
  • p=quarantine - send failing mail to the spam folder.
  • p=reject - bounce failing mail outright.

The new t= tag lets you test a setting one notch gentler before you commit to it. t=y tells receivers: “this is the policy I want, but apply the level just below it for now.” So:

You publish Receivers actually apply
p=reject; t=y quarantine
p=quarantine; t=y none (monitor only)
p=none; t=y none (already the floor)

It is a step down one level, not an off switch, and not the old pct=0. RFC 9989 spells out both what t=y asks for and what it expects. First, the request itself:

A request that the actor performing the DMARC validation check not apply the policy, but instead apply any special handling rules it might have in place, such as rewriting the RFC5322.From header field.

And the step-down it expects (the policy still applies, just one level down):

The Domain Owner is currently testing its specified DMARC assessment policy and has an expectation that the policy applied to any failing messages will be one level below the specified policy. That is, if the policy is “quarantine” and the value of the “t” tag is “y”, a policy of “none” will be applied to failing messages; if the policy is “reject” and the value of the “t” tag is “y”, a policy of “quarantine” will be applied to failing messages, irrespective of any other special handling rules that might be triggered by the “t” tag having a value of “y”.

Two consequences the RFC states outright are worth pulling up. t= “has no effect on any policy (‘p’, ‘sp’, or ‘np’) that is ‘none’” - so p=none; t=y simply stays none. And it “does not affect the generation of DMARC reports” - which is exactly why the test phase works: the aggregate reports you are watching keep flowing the whole time.

So p=reject; t=y still protects you at the quarantine level - failing mail is quarantined, or handled by the receiver’s own special rules such as a From rewrite - it does not stop enforcing entirely. Which of those a given mailbox provider does can vary, so do not assume identical behaviour across every receiver. At least one well-known vendor blog already describes t=y as equivalent to pct=0, which is wrong, and it is the kind of error that quietly loosens a domain’s enforcement by a full level.

A worked example. Acme is at p=none and wants to reach p=reject safely.

The old way (RFC 7489) was to ramp the pct tag - reject a growing slice of failing mail:

v=DMARC1; p=reject; pct=25; rua=mailto:[email protected]   # reject 25% of failures

Then edit pct to 50, 75, 100 over several weeks. pct is now removed from the standard, so this no longer works as intended.

The new way (RFC 9989) is to publish your real target, p=reject, with the test flag on:

v=DMARC1; p=reject; t=y; rua=mailto:[email protected]     # acts as quarantine while testing

Every failing message is now quarantined - one notch below reject - while Acme watches the reports. When the reports show only spoofed mail is failing, Acme deletes three characters:

v=DMARC1; p=reject; rua=mailto:[email protected]          # full reject, live

The record said reject the whole time; the t=y flag was just a temporary hold. That is the point of t= over simply publishing p=quarantine: your record always states your true goal, and finishing the rollout is removing a flag, not rewriting your policy - which is why so many domains that publish quarantine “temporarily” get stuck there forever.

One caution before you pair this with np=. The t= flag steps down every policy you declare, not just p=. RFC 9989 applies it to the policies “declared in the ‘p’, ‘sp’, and/or ‘np’ tags.” So a record like p=reject; np=reject; t=y also puts your non-existent-subdomain protection into test mode: for the whole rollout, np=reject behaves as quarantine, not reject. If you want np=reject enforcing at full strength while you test your main policy, a single record cannot do that today - so either accept that np rides one level down during the test window, or hold t=y off until you are ready to enforce everything at once.

For most domains none of this comes up: the default is t=n (normal enforcement), and t=y is only worth setting for the short testing window before you move from quarantine to reject. It is a temporary tool, not a tag to leave on.

np= is the new headline control

Because sp= now covers only subdomains that exist, np=reject can shut down invented ones like invoices-secure.yourdomain.com while you leave your real subdomains on something gentler during a rollout. It costs nothing, needs no monitoring, and closes the single most common subdomain-spoofing trick. psd= is mostly for registries: psd=n declares “I am my own Organizational Domain”, psd=y declares a public suffix, and psd=u, the default, lets the tree walk decide.

What it means in practice

Nothing you have already deployed is broken. Receivers that implement RFC 9989 ignore retired tags like pct, while older receivers still on RFC 7489 logic may keep honouring them - so a record carrying pct= keeps working either way, and plenty of receivers still honour it because the installed base is full of it.

The gap to watch runs the other way. t= and np= are new, and receiver support for them is still thin. So the honest position for the next year or so is a split one:

  • pct is dead in the spec but alive in the field: removed from RFC 9989, yet widely still honoured.
  • t= is alive in the spec but not yet reliable in the field: correct to publish, not yet safe to depend on.

The practical consequence is that a percentage-based ramp is no longer part of the standard and should not be relied on for new deployments. During the transition many receivers still honour pct, but the spec-compliant path is a time-based ramp: hold at quarantine, watch aggregate reports for a week or two, then move to reject.

Who supports it yet?

There is an easy trap here. Google’s, Yahoo’s and Microsoft’s well-known deadlines - February 2024 for Google and Yahoo, May 2025 for Microsoft - are sender requirements: “if you send us bulk mail, you must publish a DMARC record.” That is a completely separate question from whether their receiving systems have swapped the Public Suffix List for the tree walk and learned to honour t= and np=. A provider can demand that you publish DMARC while still evaluating it the old RFC 7489 way, so “Google requires DMARC” does not mean “Google has implemented DMARCbis.”

As of now, receiver-side adoption is early and still rolling out, and there is no blanket public confirmation that the big mailbox providers have migrated. Fortunately you do not have to take anyone’s word for it. RFC 9990 added a discovery_method field to the aggregate report, with two values: psl (the receiver still used the Public Suffix List) or treewalk (it has moved to DMARCbis). Every receiver tells you, in its own reports, which method it used - so watching discovery_method flip from psl to treewalk across your Google, Microsoft and Yahoo reports is the real per-provider adoption signal, read straight from your own data (our Postbox DMARC Monitor collects those reports for you).

In the raw aggregate XML it sits inside <policy_published>, next to the domain’s policy - grep your reports for discovery_method:

<policy_published>
  <domain>example.com</domain>
  <adkim>r</adkim>
  <aspf>r</aspf>
  <p>reject</p>
  <np>reject</np>
  <discovery_method>treewalk</discovery_method>
</policy_published>

The honest takeaway: publish to the new standard, but do not assume any given receiver enforces t= or np=, or uses the tree walk, until its own reports say discovery_method=treewalk.

What to do now

Two things are worth folding into audits today. First, recommend np=reject as standard: it is the cheapest spoofing defence available and there is rarely a reason not to set it. Second, check whether a client’s DMARC vendor has actually implemented the tree walk, because a vendor still doing Public Suffix List lookups will report alignment differently from a receiver that has moved on, which shows up as “the numbers do not match” long before anyone suspects the cause.

A record built to the new spec is nothing exotic - just the modern tags in place of the retired ones:

# at _dmarc.yourdomain.com
v=DMARC1; p=reject;
  rua=mailto:[email protected];
  sp=reject; np=reject;
  adkim=r; aspf=r
# note what is NOT here: no pct=, no rf=, no ri=

The adkim/aspf lines are shown for clarity but are optional - both default to r (relaxed) if you omit them; set them to s only if you want strict alignment.

If you want your own record checked against the 2026 spec, our DMARC enforcement guide walks the safe path to p=reject, and Postbox DMARC Monitor collects the aggregate reports you tighten the policy on.

Frequently asked questions

What is DMARCbis?

DMARCbis is the 2026 rewrite of the DMARC standard. It was published in May 2026 as three RFCs - RFC 9989 (the core protocol), RFC 9990 (aggregate reporting) and RFC 9991 (failure reporting) - and it obsoletes the original RFC 7489 from 2015. DMARC is now a Proposed Standard. Because there is no version bump (records still start v=DMARC1), there is no separate "DMARCbis" any more: it is just DMARC.

Is RFC 7489 still valid?

No. RFC 9989 formally obsoletes RFC 7489 (and the RFC 9091 PSD experiment), so 7489 is now historical. Records you published under it keep working because v=DMARC1 is unchanged, but the specification everyone should cite is RFC 9989.

Does pct= still work?

The pct= tag was removed from the standard, so a fully DMARCbis receiver ignores it and applies your policy to 100% of failing mail. In practice many receivers still honour pct= during the transition, so leaving it in place is not harmful - but do not rely on it, and use the new t= testing flag instead of a percentage ramp.

What does t=y actually do?

t=y is a testing flag. It asks receivers to apply the policy one level below the one you declared, not to switch enforcement off: p=reject; t=y is treated as quarantine, and p=quarantine; t=y as none. It has no effect on a policy that is already none, and it does not stop DMARC reports from being generated - which is the whole point of a test phase. Note that it steps down sp= and np= as well as p=.

Do I need to change my DMARC record?

Not urgently. v=DMARC1 is unchanged and unknown tags are ignored, so your current record keeps working. The worthwhile edits are additive: add np=reject to block spoofing of non-existent subdomains, stop relying on pct= in favour of a time-based ramp, and cite RFC 9989 rather than 7489.

Need help moving your sending to a properly authenticated domain?

← All industry updates

Chat with us!