---
title: "MX Lookups over DNS-over-HTTPS: What the JSON Actually Returns"
description: "Why an email validator resolves MX records over DNS-over-HTTPS instead of port 53, and what the five live answer shapes — with Status codes, TTLs and SOA authorities — mean for each verdict."
slug: "mx-lookups-over-dns-over-https"
date: 2026-10-07
updated: 2026-10-07
last_tested: 2026-10-07
summary: "Measured against Cloudflare's DoH resolver on 2026-10-07: five DNS answer shapes a validator really sees, the resolver caches they arrive from, and the verdict each one must produce."
cluster: "DNS and RFCs"
intent: "engineering"
sources:
  - title: "RFC 8484 — DNS over HTTPS"
    url: "https://www.rfc-editor.org/rfc/rfc8484.html"
  - title: "RFC 1035 — Domain names, RCODEs and record types"
    url: "https://www.rfc-editor.org/rfc/rfc1035.html"
  - title: "RFC 2308 — Negative Caching of DNS Queries (DNS NCACHE)"
    url: "https://www.rfc-editor.org/rfc/rfc2308.html"
  - title: "RFC 7505 — A Null MX Resource Record"
    url: "https://www.rfc-editor.org/rfc/rfc7505.html"
  - title: "RFC 8914 — Extended DNS Errors"
    url: "https://www.rfc-editor.org/rfc/rfc8914.html"
  - title: "Cloudflare DNS over HTTPS JSON API"
    url: "https://developers.cloudflare.com/1.1.1.1/encryption/dns-over-https/make-api-requests/dns-json/"
  - title: "nobounce.dev verdict taxonomy and fixture list"
    url: "https://nobounce.dev/v1/config"
---

Every MX check nobounce.dev runs goes over DNS-over-HTTPS (DoH), not through a stub resolver on UDP port 53. That is not a style choice. The engine runs at the edge, where a function gets an HTTP fetch and no datagram socket, so classic DNS as defined in RFC 1035 is simply unreachable — RFC 8484 carries the same query over an HTTPS request instead. The side effect is the useful part: a DoH JSON response hands the validator the RCODE, the record TTLs and the SOA of the negative answer as plain fields, which is exactly the evidence a verdict has to branch on.

## The request

```bash
curl -H 'accept: application/dns-json' \
  'https://cloudflare-dns.com/dns-query?name=gmail.com&type=MX'
```

Three fields do the work. `Status` is the RCODE from RFC 1035. `Answer` holds the records. `Authority` holds the SOA on a negative answer, and its TTL is the resolver's negative cache lifetime under RFC 2308 — the minimum of the SOA TTL and the SOA MINIMUM field.

## Five answers, measured on 2026-10-07

Every row below was resolved live today against Cloudflare's public DoH resolver, then run through the fixture corpus at `/demo/check`. No row is hypothetical.

| query | DoH returned | nobounce verdict |
|---|---|---|
| `gmail.com` MX | Status 0, five MX records, TTL 979 | `deliverable` / `ok` |
| `example.com` MX | Status 0, single answer `"0 ."` | `undeliverable` / `reserved_domain` |
| `gmail.co` MX | Status 0, answer `"0 ."` | `undeliverable` / `null_mx` |
| `outlook.con` MX | Status 3, no answer, SOA TTL 86400 | `undeliverable` / `domain_not_found` |
| `gmaill.com` MX | Status 0, no answer, SOA TTL 300; no A, no AAAA | `undeliverable` / `no_mx_no_a` |
| `gmial.com` MX | Status 2 SERVFAIL, `EDE(22): No Reachable Authority` | `unknown` / `dns_error` |

Four of those rows deserve comment beyond the table.

**`"0 ."` is a decision, not an absence.** A null MX per RFC 7505 is the domain operator publishing "this domain accepts no mail, do not fall back". Two different domains can publish the identical record and deserve different reasons: `example.com` is an RFC 2606 reserved name that must surface as `reserved_domain` or headless QA breaks, while `gmail.co` is a real registration refusing mail and returns `null_mx`. The distinction and its trap are covered in [Null MX and Reserved Domains](https://nobounce.dev/blog/null-mx-rfc-7505-reserved-domains/).

**An empty answer is not a rejection.** `gmaill.com` returns NOERROR with no MX. Under RFC 5321 the implicit-MX rule says a sender then tries the domain's own A/AAAA records — so the validator has to ask again for both before it may conclude anything. Today `gmaill.com` has neither, which is what `no_mx_no_a` means: nothing to deliver to, nothing to fall back to.

**NXDOMAIN arrives with a clock attached.** The `outlook.con` authority section carries an SOA whose negative TTL is 86400 — a full day. A resolver that cached this answer serves it for up to a day even if the domain is registered five minutes later. Your validator inherits that cache; "the domain does not exist" always means "does not exist as far as this resolver's cache says right now".

**SERVFAIL carries a reason.** The `gmial.com` failure comes back with an Extended DNS Error, `EDE(22): No Reachable Authority` — RFC 8914's way of saying the resolver could not reach the delegation, not that the delegation answered no. Status 2 and Status 3 produce opposite caller behaviour: NXDOMAIN is a reject, SERVFAIL is a fail-open. [SERVFAIL Is Not NXDOMAIN](https://nobounce.dev/blog/servfail-vs-nxdomain-email-validation/) is the full write-up of why collapsing them is the classic bug.

## What the resolver's cache does to your latency

That `gmail.com` TTL of 979 is not a constant — it is a countdown from 3600 on a cached answer. You are always reading the resolver's cache, never the authoritative zone. A validator on top of DoH therefore has two cache layers of its own to get right: nobounce keeps a shared domain cache with 6h positive and 1h negative TTLs, and a failed lookup never writes a row, so a resolver hiccup cannot pin a wrong verdict for the TTL. The `cached` field in the response tells you which layer your answer came from.

## The resolver is a dependency — plan for its outage

Because every check crosses the resolver, the resolver is on your signup path. Treat it the way you treat any third-party on that path: with a deadline and a fail-open branch. `unknown` with `checked.mx: false` is the contractual statement that DNS did not complete, and it is never reported as `deliverable`. Note that even that `gmial.com` response still carries `suggestion: user@gmail.com` — typo correction runs before DNS and does not depend on it, so a resolver outage costs you the MX verdict, not the correction. The deadline mechanics are in [How to Handle an Email Validation Timeout at Signup](https://nobounce.dev/blog/handle-email-validation-timeout/).

## What leaves your system

Only the domain crosses to the resolver — the local part is never part of a DNS query. On the nobounce side, no email address is persisted in any form: anything stored is SHA-256 hashed, and domains are kept in clear because a domain is not personal data. A DNS-based validator that never opens an SMTP session cannot leak the local part to a receiving mail server either, because no SMTP session exists.

## The caller's branch table

```bash
curl -X POST https://nobounce.dev/v1/check \
  -H "Authorization: Bearer $NOBOUNCE_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"email":"user@gmaill.com"}'
```

```json
{
  "verdict": "undeliverable",
  "reason": "no_mx_no_a",
  "suggestion": "user@gmail.com",
  "confidence": 0.94,
  "checked": { "syntax": true, "mx": true, "typo": true, "disposable": true },
  "cached": false
}
```

Branch on `reason`, not on prose: `domain_not_found`, `null_mx` and `no_mx_no_a` are three different DNS stories with the same caller action (show the suggestion, end the attempt); `dns_error` with `checked.mx: false` is the one that means "accept and continue". Do not cache a failed check on your side either — the resolver may recover before your TTL does.

For the full verdict-to-action mapping, key handling and the fail-open contract, give your coding agent the integration guide at [nobounce.dev/integrate.md](https://nobounce.dev/integrate.md).
