QNAME Minimization in Practice: RFC 7816, Implementation, Pitfalls

When you open mail.google.com, your recursive resolver takes three to four steps: it queries the root server, then the TLD server, then the authoritative server for google.com, and sometimes one additional level.

For decades, every one of these servers received the exact same full query: "give me mail.google.com". The root server, which has no clue about Google. The TLD server, which only handles com. delegations. Each of them saw the full domain name, even though they only needed a single label to do their job.

In 2016, Stefan Bortzmayer wrote RFC 7816 and said: guys, this is weird. Let's have the resolver ask for exactly as much information as is needed for the next hop. The idea is absurdly simple. And from that moment on, ten years of implementation began.

What is QNAME minimisation

A standard recursive resolver works like this. A user queries mail.google.com. The resolver goes to a.root-servers.net, sends QUERY: mail.google.com A. The root server responds: I don't know, ask gtld-servers.net (referral to NS records for com.). The resolver goes to gtld-servers.net, sends QUERY: mail.google.com A. That server responds: ask ns1.google.com. And only Google's authoritative server returns the actual IP address.

Note: every server along the path received the full mail.google.com name. Including the root server. The root server, which physically cannot return any useful information about a specific host, still knows that you are accessing it. This is a completely pointless leak.

With QNAME minimisation, the resolver plays a more polite game. The root is asked: "who is authoritative for com.?". The root responds with a delegation. The TLD is asked: "who is authoritative for google.com.?". The TLD responds. And only Google's authoritative server learns that you were actually looking for mail. Each level receives only the minimum information it needs for delegation, not a single byte more.

Why it took ten years to roll out

RFC 7816 was published in March 2016 with experimental status. Many resolver operators refused to enable the feature: there were suspicions that some authoritative servers would break on intermediate queries. If an authoritative server incorrectly implements handling of qtype=NS for an intermediate label, the resolver could receive NXDOMAIN where it should have received NODATA, and resolve nothing.

The suspicions turned out to be partially true. Between 2016 and 2018, old versions of BIND, exotic enterprise DNS, and custom authoritative servers were regularly caught having this issue. So the feature was rolled out via a flag, and the flag was set to off by default. Unbound enabled it by default in 2019. BIND followed later. All public resolvers (Cloudflare, Google, Quad9) gradually adopted it by 2020–2021.

RFC 9156 was published in 2021, already on the standards track. The industry's attitude toward it was different: the industry agreed that QNAME minimisation is a standard practice, not an experiment. By 2026, not supporting it will be roughly equivalent to not supporting TLS 1.2: it is formally allowed, but frowned upon.

What is this even for

QNAME minimisation is not security, it is privacy. It does not directly prevent any attacks. If someone wants to poison your cache, the fact that the root server sees one less label will not stop them. The Kaminsky DNS vulnerability is 17 years old, but there are no fewer surprises, and DNS vulnerabilities persist in a parallel world.

What actually changes: passive DNS surveillance becomes less effective. Root operators (there are few of them, and their list is public) only see the TLD level of queries. TLD operators see the SLD. The observation chain is fragmented. No one except the authoritative server and the resolver itself knows the final domain name.

This is critical for VPN users and Tor users. If you run your own resolver over a VPN, traffic to root servers originates from you, and without minimisation, the root server sees your full traffic. With minimisation, only the zone level is visible.

This is also beneficial for regular users who use their ISP's DNS. The fewer intermediaries that know the full query history, the better.

Implementation in Go using miekg/dns

I am writing an edge resolver using github.com/miekg/dns. The minimisation logic built on top of the library works as follows. We take the qname, split it into labels. We traverse from the root downwards, at each level querying the current authoritative server for the next label's NS record. At the final step, we query for the actual qtype.

func (r *Resolver) resolveMinimized(qname string, qtype uint16) (*dns.Msg, error) { qname = dns.Fqdn(qname) labels := dns.SplitDomainName(qname)

cur := "."
nameservers := r.rootNS()

for i := len(labels) - 1; i >= 0; i-- {
    cur = labels[i] + "." + cur

    if i == 0 {
        return r.queryAt(nameservers, cur, qtype)
    }

    resp, err := r.queryAt(nameservers, cur, dns.TypeNS)
    if err != nil {
        return nil, err
    }

    next, err := r.extractDelegation(resp)
    if err != nil {
        // fallback to full qname if no NS is returned
        return r.queryAt(nameservers, qname, qtype)
    }
    nameservers = next
}

return nil, errors.New("unreachable")

}

This is a simplified skeleton. In practice, there is caching at every level, glue records, IPv4/IPv6 for nameservers, and timeouts. But the core logic is exactly this: iterate over labels, use qtype=NS for intermediate steps, and use the actual query type for the final request.

Rakes

As usual, reality hit me with a stick to the head. These are the rakes.

Janky authoritative servers. The RFC states that for an intermediate qtype=NS query for a label that does not exist as its own zone, the server should respond with NODATA (empty ANSWER section, NOERROR rcode), as this is expected — it may simply be a node in the DNS tree with no delegated zone of its own. Older servers respond with NXDOMAIN instead of NODATA in this scenario. A resolver that trusts the NXDOMAIN response will conclude that the domain does not exist, so the user sees an error even though mail.example.com is working perfectly fine.

The solution: when we get an NXDOMAIN response for an intermediate label, we fall back to querying the full qname. This is not ideal, as privacy is lost specifically for problematic domains, but the alternative (breaking name resolution) is worse.

CNAME chains. If a CNAME is encountered along the DNS resolution path, the qname changes, and minimisation has to be restarted from scratch for the new name. This is correct behavior per the relevant RFC, but it adds a layer of recursion in the code. I have an 8 CNAME hop limit set on my edge resolver: if more hops are encountered, it returns SERVFAIL. Testing this without flags on my own setup is pretty humiliating: you sit and watch the resolver loop on a misconfigured zone.

Wildcard zones. A *.example.com record matches any sublabel. QNAME minimisation asks "who is authoritative for wat.example.com?", the authoritative server responds with a self-referral or no response, and the resolver must correctly identify that the final hop is targeting this same server. This does not work smoothly everywhere.

Performance. With a cold cache, QNAME minimisation results in 3-4 NS queries instead of 1-2. There are more hops, so latency is higher. With a warm cache, there is no difference, because intermediate NS records are cached. But the first user who queries a rare domain will incur the cost of extra milliseconds.

Benchmarks

I ran these measurements on my edge resolver in Helsinki, with a cold cache (the resolver was restarted before each test run).

Cold resolve mail.google.com:
  without QNAME min:    35 ms
  with QNAME min:      52 ms

Cold resolve gerrit.googlesource.com:
  without QNAME min:    41 ms
  with QNAME min:      68 ms

Cold resolve www.example.org:
  without QNAME min:    28 ms
  with QNAME min:      44 ms

17-27 ms of extra latency. This is the privacy cost of operating with a cold cache.

With a warm cache, there is 0 ms difference, because in both modes we hit the cache at the final resolution level. Given that over 90% of queries are served via the warm cache, the amortised cost of minimisation is close to zero.

What is enabled by default

QNAME minimisation is enabled by default for all VantageDNS plans, whether Free, Plus, or any other tier. You can disable it via your profile (qname_minimisation: false in the API), but I see no reason to do so except for cases like "I have a corporate AD setup with a 15-year-old BIND server that won’t resolve". In those scenarios, it’s usually sufficient to disable minimisation for the specific affected domain, rather than for the entire profile.

2026 status comparison:

NextDNS         enabled by default
AdGuard DNS     enabled by default
Cloudflare      enabled by default
Quad9           enabled by default
Google 8.8.8.8  enabled by default
VantageDNS      enabled by default

All reputable public resolvers support it. If a resolver doesn’t support it in 2026, they clearly have far more pressing issues to address.

Testing

Stefan Bortzmayer even created a test domain to check if minimisation is working on your resolver.

dig +short txt qnamemintest.internet.nl

If the response is:

"HOORAY - QNAME minimisation is enabled"

It works. If:

"NO - QNAME minimisation is NOT enabled"

It doesn’t work. Simple, straightforward, no fuss.

I run this check in CI after every resolver modification. If the result suddenly flips back to NO, it means I broke something.

What QNAME minimisation does not solve

To avoid any misconceptions.

The authoritative server still knows the full qname regardless. This is unavoidable: it needs the complete name to return the correct record. Privacy is only achieved relative to intermediate servers (root, TLD, and sometimes zone parent servers).

ECS leakage. If your resolver sends EDNS Client Subnet (ECS) data, the authoritative server learns not only the full qname, but also your /24 subnet. Minimisation doesn't help here. I have ECS disabled globally, because for a public recursive resolver, this is a privacy leak exchanged for CDN optimisation, and privacy takes priority.

TCP fingerprinting. The resolver performs a TCP or TLS handshake with the authoritative server. The JA3 fingerprint is observable. If someone is determined to track your resolver on the network, minimisation won't protect you from that.

Resolver logs. If the resolver logs queries, it knows everything. So resolver privacy = resolver privacy policy + minimisation, not minimisation alone in a vacuum. In our setup, we only write metadata (qname, qtype, ts, action) to ClickHouse, with a retention period of 24 hours for free tiers, up to 30 days for paid tiers, and this is enforced via TTL. Response content is never logged.

What to do about this

QNAME minimisation is a baseline standard in 2026, not a standout feature. It's not something to boast about, it's simply expected. If your DNS provider still doesn't support it, that's a clear sign you should review what other critical features they're missing. If you run your own resolver, run the test at qnamemintest.internet.nl. It only takes ten minutes to complete the check, and you'll know you're not sending the full list of your visited domains to root operators.

Links

Comments

    Also read