MX Record Lookup vs Email Validation
by EmailGuard Engineering
Share this article
Enjoyed this article?
More notes on building products, infrastructure, and teams.
by EmailGuard Engineering
More notes on building products, infrastructure, and teams.
An MX record lookup asks DNS which hosts accept mail for a domain. It does not ask whether jane@ exists on that domain. Email validation at signup still needs syntax and classification. Mailbox existence is a third job, and it is SMTP. EmailGuard does the first two and can return mx_present from a cached DNS snapshot. Omitted mx_present means we did not have a snapshot, not that the live internet has no MX.
This page sits next to email validation vs verification. Buy the job, not the vendor word.
mx_present, wildcard_dns, and infra_cluster_match flags differ from dns.resolveMx in your process.An MX (mail exchanger) record is a DNS resource record. It names the host that should receive mail for a domain and a preference number. Lower preference is tried first. That is routing data for senders. It is not a directory of local-parts.
A lookup answers: "If I send to @example.com, which hosts did the domain publish?" It does not answer: "Is jane a mailbox on that host today?"
RFC 5322 defines the address format (local-part @ domain). Format plus MX still leaves the local-part unproven.
RFC 5321 §5.1 tells senders how to locate the target host. If there are no MX records, the client falls back to A or AAAA for the domain name itself and treats that as implicit MX. A Node one-liner that only calls dns.resolveMx and rejects on empty results will refuse some domains that still accept mail.
Two other DNS outcomes get collapsed in blog code samples:
| DNS result | What it means | Signup instinct |
|---|---|---|
| NXDOMAIN | The name does not exist | Safe to reject after syntax |
| NODATA / empty MX, domain exists | No explicit MX | Check A/AAAA before you call it dead |
| Timeout / SERVFAIL | You learned nothing | Unknown. Do not treat as disposable |
| MX present | Domain published mail routing | Domain can receive mail in principle |
Vendor posts sometimes quote a share of "no MX" domains that never accept mail. We have not reproduced that measurement. Treat it as marketing unless you ran the same resolver study.
RFC 7505 defines Null MX: preference 0 and a target of . (the root). That is an explicit "this domain does not accept mail." It is cleaner than publishing nothing, because nothing triggers the A/AAAA fallback. If you write your own resolver, special-case Null MX. Do not treat it as "empty MX, try A."
EmailGuard classification does not open port 25 to test Null MX live on every signup. If you need that edge in your own resolver, implement RFC 7505 there.
Signup blogs often stack three layers and call the stack "validation."
| Check | Network | Proves | Lies about |
|---|---|---|---|
| Syntax | None | Storeable addr-spec | Typos that still parse (gnail.com) |
| MX / implicit MX | DNS | Published (or implicit) mail routing | Local-part, catch-all, throwaway farms with real MX |
| Classification | Lists + DNS intel | Kind of address (disposable, role, relay, freemail) | Inbox existence |
| SMTP RCPT / VRFY | Port 25 | What that MX said to your IPs today | Gmail/Microsoft anti-probe, greylist 4yz (RFC 6647) |
A disposable domain can publish perfect MX. 10minutemail style hosts exist to receive the confirmation message. MX-pass then SMTP-pass still stores a user you cannot email next week.
A corporate catch-all publishes MX and accepts any local-part. MX-pass tells you the company runs mail. It does not tell you asdkjfh@bigco.com is a person.
We do not sell the SMTP row. If you need mailbox ping, that is Kickbox, ZeroBounce, NeverBounce, and peers. See alternatives. Prove a mailbox you care about with a confirmation link.
GET /api/v1/emails/detect classifies the address. When we have a cached DNS snapshot for the domain, the 2xx body may include:
| Field | Meaning |
|---|---|
mx_present | Snapshot saw mail-routing data we store as present |
wildcard_dns | Snapshot looked like wildcard DNS typical of temp-mail farms |
infra_cluster_match | Domain shares MX/A infrastructure with a known disposable cluster |
These flags are documented on the detect API and in confidence and fail-open.
Omitted is not false. If the snapshot is missing, we omit the flags. Do not write if (!data.mx_present) reject(). That treats "we did not cache this domain" as "no mail route."
They are not SMTP. We do not VRFY or RCPT. mx_present: true is not deliverable. infra_cluster_match is a classification hint, not a mailbox probe.
detection_source stays precomputed or live_dns for the disposable path. Do not mix that field up with "we just ran dig MX in your region."
dns.resolveMx into the browserA browser MX check via DNS-over-HTTPS still leaks the typed domain to a third resolver and still misses classification. Run server-side detection. Keep the API key off the client.
A reasonable Node sketch for your own pre-check, if you insist on a local MX gate before calling anyone:
import { promises as dns } from "node:dns";
export async function domainHasMailRoute(domain: string): Promise<
"yes" | "no" | "unknown"
> {
try {
const mx = await dns.resolveMx(domain);
if (mx.some((r) => r.exchange === "" || r.exchange === ".")) {
return "no"; // Null MX style
}
if (mx.length > 0) return "yes";
} catch (err: unknown) {
const code = (err as { code?: string }).code;
if (code === "ENOTFOUND") {
try {
await dns.resolve4(domain);
return "yes"; // implicit MX, RFC 5321 §5.1
} catch {
try {
await dns.resolve6(domain);
return "yes";
} catch (inner: unknown) {
const innerCode = (inner as { code?: string }).code;
if (innerCode === "ENOTFOUND") return "no";
return "unknown";
}
}
}
return "unknown";
}
return "unknown";
}Use unknown the way you use a fail-open timeout. Then call detect for disposable, role, relay, and public domain. That is the email validation API checklist.
| Signal | Action |
|---|---|
| Syntax fail | 422, ask them to fix. If suggested_email is present, offer it. See email typo suggestions. |
| Detect timeout / 5xx / 429 | Fail open on ordinary signup. Recheck later. |
disposable: true | Block. MX will often be fine. |
wildcard_dns or infra_cluster_match with disposable | Block. You already have the disposable flag. |
mx_present omitted | Ignore. Do not invent a reject. |
mx_present: false on a snapshot | Strong hint the domain is not a mail destination. Still combine with syntax. Confirm with your own resolver if you hard-block. |
| Role / relay / public domain | Policy, not MX. See role signup policy and work email. |
Checkout and KYC can fail closed on transport errors. Ordinary signup should not.
Use these as staging fixtures. Do not treat them as a forever-accurate catalog.
| Address-shaped input | What MX-only code often does | What you want |
|---|---|---|
user@gmail.com | Pass (MX present) | Classify: public_domain, not disposable |
user@gnail.com | Fail or pass depending on whether the typo domain publishes MX | Syntax / typo path first |
| A current Guerrilla-style host | Pass (they want the confirmation mail) | disposable: true |
@privaterelay.appleid.com | Pass | relay_domain, allow or confirm |
| Made-up TLD, NXDOMAIN | Reject | Reject |
| Domain with only A, no MX | Reject if you skipped RFC 5321 | Implicit MX, then classify |
Microsoft 365 Directory-Based Edge Blocking can 550 invalid recipients at the edge when the accepted domain is Authoritative. That is an SMTP-time behavior after MX is already published. It does not show up in resolveMx. Internal relay tenants turn DBEB off. "M365 is catch-all" is tenant config, not an MX fact. Google documents catch-all routing for unknown local-parts. Again, MX is present either way.
If your threat model is bounce rate on a blast, buy a verifier and accept unknown. If your threat model is trial abuse, MX is a weak filter and classification is the filter.
MailCop's 2026 MX article (fetched for this piece) tells you to run a 3-second DNS timeout at signup, skip SMTP on the request path, and mark unresolvable names as pending. That timeout budget is for their resolver plus an optional later SMTP job. EmailGuard's detect call is a classification GET. Live DNS inside our pipeline has its own budget (EMAIL_DNS_TIMEOUT_MS on our side). Your client abort should be 400–800 ms if you want that work to finish, not 3 seconds copied from an SMTP guide. See fail-open timeouts.
They also publish a 94% figure for domains without MX that never accept mail. That is their study, not ours. We are not repeating it as a fact.
DEV posts in this SERP often stop at "MX then SMTP" and treat classification as a footnote. Signup abuse lives in the footnote.
Rejecting empty MX with no A/AAAA fallback. You are not implementing RFC 5321.
Treating MX timeout as invalid. You are implementing packet loss as fraud.
Skipping disposable checks because MX passed. Throwaway providers publish MX on purpose.
Caching mx_present for weeks. Snapshots go stale. Recheck on a short TTL. See how to cache detect results.
Calling SPF a recipient check. RFC 7208 authorizes sending hosts. It does not list inboxes.
No. It proves the domain published mail routing, or that you are looking at implicit MX / a snapshot of that fact. The local-part is unproven.
Block NXDOMAIN and Null MX after you understand the lookup. Do not block on timeout. Do not block on omitted mx_present from EmailGuard. For empty MX, follow RFC 5321 A/AAAA fallback before you decide.
No. Verification blogs usually mean SMTP after MX. That is a mailbox ping. Classification (disposable, role, relay) is a different job. Vocabulary: validation vs verification.
No cached DNS snapshot for that domain. Absence of the field is not a negative MX result.
You can inspect a single domain in any dig-style tool. Signup needs a server-side policy and disposable/relay flags. Start from pricing or the disposable checker while you wire detect.
resolveMx as your whole signup gate.disposable, relay_domain, and the DNS flags when they appear.