Most DNS answers in email validation are ambiguous. Null MX is not. It is the one case where a domain owner has explicitly published a machine-readable statement that no mail should ever be sent here, and a validator that ignores it is discarding the clearest signal available.

What a null MX record looks like

RFC 7505 defines it precisely: a single MX record, preference 0, with the target set to the root domain — a lone dot.

$ dig +short MX gmail.co
0 .

The target . cannot be resolved to a mail host, which is the point. Under RFC 5321 a sender resolves MX targets to addresses and connects to them; a target of root has no addresses and never will. The record exists so that a sender can stop immediately instead of falling back to the implicit MX rule and trying the domain's A record.

That fallback is exactly what makes null MX necessary. Without it, a domain with a web server and no mail server looks deliverable: no MX record means "use the A record as the mail exchanger", so mail gets thrown at a machine on port 25 that will never answer. Null MX turns an eventual timeout into an instant, cheap refusal.

Note which typo domains use it. gmail.co and yahoo.co both publish 0 ., verified against live DNS on 2026-08-06 and re-checked on 2026-08-24. Their owners are parking country-code domains for the web and have deliberately declined mail. An MX-only validator gets these two right by accident, which is worth knowing when you are estimating how much a naive check actually catches.

Three answers that all mean "no mail here"

Null MX is one of three distinct no-mail conditions, and they are worth separating because they have different causes:

DNS state reason code what it means
MX 0 . null_mx the owner explicitly refuses mail, per RFC 7505
NOERROR, no MX, no A/AAAA no_mx_no_a nothing to fall back to under RFC 5321
MX points at a host with no A/AAAA no_mx_no_a the mail exchanger cannot be reached

The third row is hotmai.com, whose MX record points at itself while the name has no address records. Deliverability is nil, but the shape of the failure is different from an explicit refusal, and reporting them under one code would throw away information a customer might want.

Then reserved domains ruin it

RFC 2606 reserves example.com, example.net, example.org, and the top-level domains .test, .invalid and .localhost. RFC 6761 adds the special-use rules. These names exist so documentation and test suites have addresses that are guaranteed never to belong to anyone.

And they publish a null MX. example.com returns 0 ., correctly and deliberately.

So a validator that only knows about null MX will report user@example.com as undeliverable with reason null_mx. Technically true. Operationally useless, and occasionally destructive.

Here is the failure that motivated the distinct reason code, taken from a real incident in a project that predates nobounce. A headless test suite signs up a user with qa@example.com. Email validation ships. Every test that creates a user starts failing, with a verdict that reads as a legitimate deliverability finding rather than a configuration problem. The team's first instinct is to look at the mail provider, because null_mx sounds like a mail problem. Time is lost proportional to how plausible the wrong explanation is.

The distinct code and what it buys

nobounce checks reserved names before any DNS lookup and returns its own reason:

{
  "verdict": "undeliverable",
  "reason": "reserved_domain",
  "suggestion": null,
  "confidence": 1.0,
  "checked": { "syntax": true, "mx": false, "typo": true, "disposable": true }
}

Two things follow from having the code. First, the message is unambiguous: this address is reserved by an RFC, look at your fixtures rather than your mail configuration. Second, and more practically, a caller can allowlist it:

// A test environment may deliberately want reserved addresses to pass.
const isTestFixture = result.reason === "reserved_domain";
if (result.verdict === "undeliverable" && !isTestFixture) {
  return reject(result);
}

You cannot write that branch against null_mx, because null_mx also covers gmail.co — a genuine typo you very much want to keep rejecting. Allowlisting null_mx to unbreak your test suite would quietly reopen a real typo class in production. The two facts need two names.

Note also that checked.mx is false here, because the check short-circuited before the network call. That is honest: no MX lookup happened. The verdict is still definitive at confidence 1.0, since it comes from an RFC rather than from DNS.

The ordering rule

Reserved-name detection runs second, after syntax and before typo correction and DNS. Two reasons: it costs nothing, and running it before the network means a test suite hammering example.com never generates DNS traffic at all.

The full frozen list of reasons is served at /v1/config, so your integration can assert against the enum instead of matching strings. The taxonomy is append-only by policy — new reasons get added, existing ones are never reworded, because customers branch on them.