Production engineering guide for dns over https covering patterns, implementation strategies, and operational best practices.
DNS over HTTPS (DoH) is a protocol that encapsulates DNS queries and responses inside HTTP/2 or HTTP/3 requests, enabling encrypted, efficient, and reliable name resolution over the Internet. It matters when network privacy, performance, and resilience are critical—such as in mobile networks, cloud-native applications, and distributed systems where DNS is a frequent, latency-sensitive service. DoH is particularly valuable when clients must resolve names across untrusted networks, such as public Wi-Fi, or when the DNS infrastructure must be flexible, scalable, and interoperable without relying on traditional UDP-based DNS.
To resolve a domain name using DoH, a client sends an HTTP request to a DoH server with a DNS query in the request body. The server responds with a DNS response, also in the HTTP body. The request is typically made over HTTPS using HTTP/2 or HTTP/3, with the Content-Type header set to application/dns-message.
curlcurl -v \
-H "Content-Type: application/dns-message" \
-H "Accept: application/dns-message" \
--data-binary @query.bin \
https://dns.example.com/dns-query
The @query.bin file contains a raw DNS message in binary format, generated by a DNS client or library. The --data-binary flag ensures no transformation of the binary payload, which is essential because DNS messages are binary by design.
The server responds with a DNS answer in the HTTP body, and the client parses it to extract the IP address for the requested name.
A DNS message consists of a header, question, answer, authority, and additional sections. The header is 12 bytes long and contains fields like id, flags, qdcount, ancount, nscount, and arcount. The question section contains the domain name and query type (e.g., A, AAAA, TXT).
To generate a binary DNS query for example.com of type A, you can use dnsmasq to create a wire-format message:
dnsmasq --dns-rr=example.com,A --dns-port=53 --dns-server=127.0.0.1 \
--dns-query=example.com,A --dns-protocol=dns --dns-format=bin \
--dns-output=query.bin
Alternatively, use dig to export a binary query:
dig @1.1.1.1 example.com A +binary > query.bin
The resulting query.bin file is a 28-byte binary blob (header + question), which curl sends to the DoH endpoint.
A DoH server must handle incoming HTTP requests, parse the DNS message from the body, resolve the query, and return the result in the same format. The server can be built with any HTTP framework. A minimal Go server might look like this:
package main
import (
"context"
"encoding/binary"
"io"
"log"
"net/http"
"syscall"
"time"
)
func dohHandler(w http.ResponseWriter, r *http.Request) {
// Read DNS message from request body
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read request body", http.StatusBadRequest)
return
}
// Parse DNS header
if len(body) < 12 {
http.Error(w, "Invalid DNS message: too short", http.StatusBadRequest)
return
}
id := binary.BigEndian.Uint16(body[0:2])
// Flags, qdcount, ancount, etc. can be extracted from body[2:12]
// Parse questions
questionOffset := 12
questionCount := int(binary.BigEndian.Uint16(body[2:4]))
var qname string
for i := 0; i < questionCount; i++ {
// Read label length
l := int(body[questionOffset])
questionOffset++
// Read label bytes
label := body[questionOffset : questionOffset+l]
questionOffset += l
// Append to qname
qname += string(label) + "."
// Skip type and class
questionOffset += 2 + 2
}
// Simulate DNS resolution
ip := resolve(qname)
// Build response
response := make([]byte, 12 + 12 + 4) // Header + question + answer
copy(response, body[:12]) // Copy original header
response[2] = 0x80 // Set QR flag (response)
response[4] = 0x01 // Set qdcount = 1
response[5] = 0x01 // Set ancount = 1
// Add answer RR
offset := 12 + 12 // after header and original question
response[offset] = 0xC0 // compressed name pointer
response[offset+1] = 0x0C // to start of question
offset += 2
response[offset] = 0x00 // type: A
response[offset+1] = 0x01
response[offset+2] = 0x00 // class: IN
response[offset+3] = 0x01
response[offset+4] = 0x00 // TTL: 1 hour
response[offset+5] = 0x01
response[offset+6] = 0x00
response[offset+7] = 0x00
response[offset+8] = 0x00 // RDLENGTH: 4
response[offset+9] = 0x04
// IP address (e.g., 93.184.216.34)
copy(response[offset+10:], ip)
// Write response
w.Header().Set("Content-Type", "application/dns-message")
w.Header().Set("Content-Length", string(len(response)))
w.WriteHeader(http.StatusOK)
w.Write(response)
}
func resolve(name string) []byte {
// Simulate lookup
switch name {
case "example.com":
return []byte{93, 184, 216, 34} // 93.184.216.34
case "cloudflare.com":
return []byte{104, 154, 16, 15}
default:
return []byte{127, 0, 0, 1}
}
}
func main() {
http.HandleFunc("/dns-query", dohHandler)
log.Fatal(http.ListenAndServe(":443", nil))
}
This handler reads the DNS message from the HTTP body, parses the question, resolves the domain name, constructs a response with an A record, and sends it back in binary DNS format.
A DoH client must be able to:
The most common configuration is a list of DoH endpoints, often provided via a doh.json file:
{
"servers": [
{
"name": "Cloudflare",
"url": "https://cloudflare-dns.com/dns-query",
"ca": "/etc/ssl/certs/cloudflare-ca.pem",
"timeout": 3000,
"retry": 2
},
{
"name": "Google",
"url": "https://dns.google/dns-query",
"ca": "/etc/ssl/certs/google-ca.pem",
"timeout": 2000,
"retry": 3
}
]
}
The client uses the Content-Type: application/dns-message and Accept: application/dns-message headers to signal the format. It can also set User-Agent to identify the client software.
A DoH client can discover the DoH server for a domain using DNS TXT records. The standard is defined in RFC 8484 and specifies that a client queries for a TXT record at _doh._tcp.<domain>.
For example, to discover DoH servers for example.com, the client queries:
dig TXT _doh._tcp.example.com
The response might be:
_doh._tcp.example.com. 300 IN TXT "priority=10, weight=5, target=do.example.com., path=/dns-query"
The client parses the TXT record to extract target, path, and priority. It then constructs the DoH URL as https://target/path, using the path and target fields.
If multiple records exist, the client selects the one with the highest priority and, in case of ties, the highest weight.
DoH introduces several failure points not present in traditional DNS:
/dns-query is missing).id mismatch, flags not set correctly, qdcount not matching the number of questions.TC flag in the header is set. The client must reissue the query with EDNS0 to support larger messages.Content-Type Header: The server may fail to parse the request body if Content-Type: application/dns-message is missing.NS, SOA) ahead of time using HTTP/2 push.DoH reduces DNS latency by eliminating the need for separate UDP connections and enabling persistent HTTP sessions. It also enables seamless integration with existing web infrastructure—CDNs, load balancers, and TLS termination.
Clients can cache DoH responses using HTTP caching headers (Cache-Control, ETag, Expires). Servers can use ETag headers to provide efficient incremental updates.
A DoH client can use Cache-Control: max-age=3600 to cache answers for one hour. If the server’s response includes an ETag, the client can send a If-None-Match header on subsequent queries:
GET /dns-query HTTP/2
Host: dns.example.com
Content-Type: application/dns-message
Accept: application/dns-message
If-None-Match: "abc123"
If the ETag matches, the server responds with 304 Not Modified, reducing bandwidth.
Extended DNS (EDNS0) allows larger messages, additional options, and improved error handling. A DoH client can include EDNS0 in the query by adding an OPT record in the additional section.
For example, to request a 4096-byte buffer size:
; OPT record
OPT 0x0000 0x0001 0x0000 0x0000 0x0000 0x0000 0x0000
- UDP size: 4096
- Extended RCODE: 0
- Version: 0
- Flags: 0
The server uses this information to send larger responses and to include additional EDNS options like DNSSEC, DNSCOOKIE, and PADDING.
A client can send multiple DNS queries in a single HTTP request. The server returns a single response with all answers. This is useful for resolving multiple domains in parallel.
The client builds a DNS message with multiple questions in the question section. The server processes all questions and returns a single response with all answers, authority, and additional records.
A DoH server can expose a /config endpoint that returns a JSON document with current server configuration, including:
Clients can periodically poll this endpoint to adjust their behavior dynamically.
DNS over HTTPS is a robust, standardized way to deliver encrypted, reliable, and high-performance DNS resolution. It works across any HTTP/2 or HTTP/3 capable infrastructure, and its success hinges on precise handling of binary DNS messages, correct HTTP headers, and proper error reporting. The protocol excels in environments where DNS is a critical path service, and its integration with existing web infrastructure makes it a natural choice for modern, distributed systems.
This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy