Back to guides
Comparison

DNSCrypt vs DoH vs DoT vs DoQ

Plain DNS travels in the open, so anyone between you and your resolver can read or change it. There are four main ways to close that gap, plus DoH3, a faster form of one of them. They protect the same thing but make different trade-offs in how they travel, how easily they can be blocked, and how widely they are supported.

By Ozy-666, creator and operator of dnsdoh.art · Published · 31 min read
YOUR DEVICE RESOLVER DoT TCP + TLS · 853 DoH HTTPS · 443 DoH3 HTTP/3 · 443 DoQ QUIC · 853 DNSCrypt UDP / TCP · 443

Five lanes to the same place; the two QUIC-based lanes (DoH3, DoQ) move quickest. The difference is the road each takes and how hard it is to close.

They all carry the same payload, a DNS question and its answer, inside encryption. What separates them is the transport they ride on, the port they use, and the practical consequences of those two choices.

Why DNS gets encrypted at all

Traditional DNS runs over port 53 with no protection. The name you are looking up, and the address that comes back, travel as plain text. Anyone positioned on the path, the local network, the internet provider, anything in between, can read which sites you are reaching and can even alter the answer to send you somewhere else.

Encryption fixes two things at once: it hides the contents of the lookup from everyone on the path, and it keeps the reply from being tampered with on the way to your resolver. (Proving the records themselves are authentic end to end is a separate job, done by DNSSEC.) One point is worth keeping in mind throughout: encryption protects the lookup from the network, not from the resolver you chose. The resolver still sees your queries, so which resolver you trust matters as much as the protocol you use to reach it.

The five protocols

DNS over TLS (DoT)

RFC 7858 · 2016 · port 853

DoT wraps ordinary DNS in a TLS connection on its own dedicated port. It is the most straightforward of the group: the message format does not change, it is simply carried inside TLS. Because it has a port of its own, it is easy to reason about and easy to operate, which is why it became the standard for system-level encrypted DNS, including Android's Private DNS and systemd-resolved on Linux.

The same dedicated port is its weakness. Traffic on port 853 is unmistakably DNS, so a restrictive network can identify and block it without touching anything else.

DNS over HTTPS (DoH)

RFC 8484 · 2018 · port 443

DoH sends the lookup as an HTTPS request on port 443, the same port that carries nearly all web traffic. To anything watching the network it looks like an ordinary connection to a website, so it is among the hardest to single out and block. It is also the only one browsers implement directly, which is why Chrome, Edge, and Firefox can use encrypted DNS without any help from the operating system.

The cost is a little more overhead from the HTTP layer wrapped around each query, and the fact that a browser doing its own DoH can quietly disagree with the rest of the system, a common source of confusion and of leaks.

DNS over QUIC (DoQ)

RFC 9250 · 2022 · port 853

DoQ carries DNS over QUIC, the modern UDP-based transport that also underpins HTTP/3. It keeps the encryption and authentication of TLS while removing the head-of-line blocking that can stall TCP-based transports, and it sets up connections faster. That makes it noticeably better on lossy or high-latency links, such as mobile networks, where a dropped packet should not hold up everything behind it.

It is newer than DoH and DoT, so support is still growing, and like DoT it uses port 853, which a network can block. The next entry, DoH3, keeps QUIC's speed while moving back to port 443.

DNS over HTTP/3 (DoH3)

DoH (RFC 8484) carried over HTTP/3 · port 443

DoH3 is not a separate protocol so much as the best of the two before it combined: it is DNS over HTTPS, but the HTTPS rides on HTTP/3, which runs on QUIC. So it keeps DoH's defining advantage, port 443 that blends in with all other web traffic and resists blocking, while gaining QUIC's faster connection setup and its resistance to a single lost packet stalling the rest.

In practice it is the strongest all-round choice where it is available: hard to block like DoH, quick to recover like DoQ. Support depends on both the resolver and the client speaking HTTP/3, which is now common in current browsers and clients. This is the form this service runs natively.

DNSCrypt

community protocol (not an IETF standard) · v2 · usually port 443

DNSCrypt is the oldest of the group and the only one that is not an IETF standard. It predates DoH and DoT and uses public-key cryptography with signed certificates: the client verifies the resolver's certificate before trusting its answers. It is driven by a community client rather than being built into operating systems, so it needs that client installed to work.

Its standout feature is Anonymized DNSCrypt, which routes the query through a relay so the resolver answering it never sees your address, separating who is asking from what is asked. The standardized answer to this, Oblivious DoH, was specified but barely shipped, so in practice this remains DNSCrypt's own territory.

Side by side

  DoT DoH DoH3 DoQ DNSCrypt
TransportTCP + TLSHTTPS (TCP)HTTP/3 (QUIC)QUIC (UDP)UDP / TCP
Port853443443853443
StandardRFC 7858RFC 8484RFC 8484 + 9114RFC 9250Community
In the browserNoYesYesNoNo
Built into OSCommonGrowingGrowingLimitedClient
Blocking resistanceMediumHighHighMediumHigh
Latency on lossy linksFairFairBestBestFair
Source anonymity optionNoNoNoNoRelays

Ratings are practical generalisations, not absolutes; a specific resolver or network can shift any of them.

DNSCrypt or DoT, if those are your two

This pairing gets asked more than any other, and the table above spreads the answer across several rows. Short version: the choice is about deployment, not cryptography. Both encrypt the same payload to the same resolver, and neither is meaningfully weaker than the other.

Pick DoT if you want the whole device covered with nothing installed. It is an IETF standard, RFC 7858, and it is already in the operating system: Android exposes it as Private DNS, a hostname in one field, and Linux has it in systemd-resolved. There is no client to install, no configuration file, and nothing to keep updated.

Pick DNSCrypt if you need a relay, or if port 853 is blocked. It is a community protocol rather than an IETF standard, it needs dnscrypt-proxy installed and configured, and it usually rides port 443 where it is not trivially separable from ordinary web traffic. That "usually" is worth a number: of the 452 DNSCrypt entries in the signed public resolver list on this machine, 420 use port 443 and 32 do not - Quad9, the resolver measured below, is one of the 18 on 8443. DoT's fixed port 853 is the opposite: easy to spot, easy to block.

The one capability that genuinely has no DoT equivalent is Anonymized DNSCrypt, which sends each query through a relay so the resolver that answers never learns your address. That separates who is asking from what is asked. Oblivious DoH was the standardised attempt at the same split and never saw real deployment, so relays remain DNSCrypt's own territory. If that property is why you are asking, the comparison is already decided.

Where our numbers stop. This service runs DoT, DoH, DoH3 and DoQ, and it does not run a DNSCrypt endpoint. The four-transport timings further down the page were taken against this resolver and DNSCrypt is not among them. The wire-cost figures in the next section are a separate measurement against somebody else's resolver, and the two sets do not belong in the same table. dnscrypt-proxy is actively maintained: release 2.1.18 in July 2026, with commits as recent as 25 August 2026.

What each transport costs on the wire

Everything above this line is the sort of comparison you can read anywhere. This part is ours: one client, one resolver address, six transports, counting what each actually puts on the wire for a single cold lookup.

How the axis was held still. Quad9 answers all six on 149.112.112.112. Every arm is the same client, q, asking the same question, example.com A, from a fresh process so nothing is resumed. Capture is tcpdump on the egress interface filtered to that one address. Byte counts are IP-layer and exclude the Ethernet header. Medians of 12 runs each, measured 27 August 2026. The address is pinned on purpose: dns.quad9.net rotates between Quad9's anycast addresses, and an earlier version of this measurement lost three of its six arms to exactly that, reporting success while the capture filter watched an address the client had stopped using.

 PacketsBytes totalClient sendsClient receives
Do53216868100
DNSCrypt41,052423629
DoT207,1862,2414,925
DoH257,7812,6055,177
DoQ1811,7697,5504,228
DoH32412,4207,8124,605

Medians of 12 cold lookups per transport, same client, same resolver address.

DNSCrypt is the cheapest encrypted option here, by about a factor of seven. Four packets and 1,052 bytes against DoT's 20 and 7,186. It has no TLS handshake to pay for: one plain DNS query fetches the resolver's certificate and the encrypted query follows. Everything built on TLS spends most of its cold cost receiving a certificate chain, roughly 5 kB of both the DoT and the DoH totals.

The two QUIC transports invert the direction. DoQ and DoH3 are the only arms where the client sends more than it receives, 7,550 and 7,812 bytes against 4,228 and 4,605. That is the protocol's own anti-amplification rule rather than anything about DNS: RFC 9000 requires a client to pad any datagram carrying an Initial packet so it is at least 1,200 bytes, and the handshake takes several. In these captures they leave as a run of 1,308-byte datagrams.

Plain Do53 is in the table as the floor, not as a recommendation: two packets and 168 bytes, nothing encrypted and nothing authenticated.

Ranges across the 12 runs: Do53 168 to 168, DNSCrypt 988 to 1,172, DoT 7,164 to 7,270, DoH 7,740 to 7,897, DoQ 11,000 to 12,689, DoH3 11,468 to 13,254. On the TCP and QUIC arms that spread is bookkeeping - how many bare acknowledgements and teardown packets fall inside the capture window - while DNSCrypt's whole spread is the size of the answer.

Correction, 27 August 2026. An earlier version of this section, live for a few hours earlier today, said close to the opposite: that a cold DNSCrypt lookup costs more bytes than a TLS 1.3 handshake, that the client sends 8,324 of them, that every DNSCrypt query is padded to exactly 1,280 bytes, and that no single client speaks both protocols, which was given as the reason the comparison had to be made at the wire at all.

That last claim is wrong, and it caused the rest. q speaks DNSCrypt given an sdns:// stamp. The first measurement ran dnscrypt-proxy on one side and q on the other and then attributed the difference to the protocols. With q on both sides DNSCrypt sends 423 bytes where dnscrypt-proxy sends 8,324, and the conclusion reverses.

The 1,280-byte query padding, and the three certificate probes of which two are padded to 3,268 bytes and fragment, are dnscrypt-proxy's behaviour, not DNSCrypt's. That is a genuine and large difference between two clients of the same protocol, and it matters if you run dnscrypt-proxy - but it is not what the section claimed. The steady-state rows it carried, 264 bytes per DoT lookup against 1,564 for DNSCrypt, are withdrawn for the same reason and are not replaced: measuring a second lookup on an open connection needs a client that holds one open, and these fresh-process arms do not.

The rule we broke is one of our own, written down before this: a test that varies one axis identifies the trigger, never the culprit. The client was the other axis and it was never controlled.

Run it against your own network

All six arms are one tool against a public resolver, so this reproduces anywhere. Save it as measure.sh and run it as root.

What you need. q for every transport, including DNSCrypt - it takes an sdns:// stamp directly, which is the part we got wrong the first time. Then tcpdump and tshark (Debian and Ubuntu: apt install tcpdump tshark), plus ip from iproute2 and any awk. Root is required because the capture is. The script checks all of this before it measures anything.

#!/usr/bin/env bash
# measure.sh - what one cold DNS lookup costs on the wire, per transport.
# One client, one resolver address, so only the transport varies.
#
#   ./measure.sh            measure and delete the captures
#   KEEP=1 ./measure.sh     keep them, and print where they are
#
# Keep them whenever a number looks wrong. The counting here is deliberately
# crude - every packet to and from the address - and the pcap is the only way
# to see what it actually counted.
set -e

for t in q tcpdump tshark ip awk; do
  command -v "$t" >/dev/null || { echo "missing tool: $t"; exit 1; }
done
[ "$(id -u)" -eq 0 ] || { echo "run as root - tcpdump needs it"; exit 1; }

KEEP="${KEEP:-0}"
D=$(mktemp -d)
cleanup() {
  if [ "$KEEP" = 1 ]; then printf '\ncaptures kept in %s\n' "$D"
  else rm -rf "$D"; fi
}
trap cleanup EXIT

# The stamp carries its own address. Change IP and you must change STAMP too,
# or the DNSCrypt arm will talk to one resolver while the capture watches another.
IP="149.112.112.112"
IF=$(ip route get "$IP" | awk '{for(i=1;i<=NF;i++) if($i=="dev") {print $(i+1); exit}}')
STAMP='sdns://AQMAAAAAAAAAFDE0OS4xMTIuMTEyLjExMjo4NDQzIGfIR7jIdYzRICRVQ751Z0bfNN8dhMALjEcDaN-CHYY-GTIuZG5zY3J5cHQtY2VydC5xdWFkOS5uZXQ'

echo "Interface $IF, resolver $IP"
echo "----------------------------------------------------------------------"

for proto in do53 dnscrypt dot doh doh3 doq; do
  case $proto in
    do53)     a=(@"$IP") ;;
    dnscrypt) a=(@"$STAMP") ;;
    dot)      a=(@tls://"$IP" --tls-server-name dns.quad9.net) ;;
    doh)      a=(@https://"$IP"/dns-query --tls-server-name dns.quad9.net) ;;
    doh3)     a=(@https://"$IP"/dns-query --http3 --tls-server-name dns.quad9.net) ;;
    doq)      a=(@quic://"$IP" --tls-server-name dns.quad9.net) ;;
  esac

  tcpdump -i "$IF" -n -s0 -w "$D/$proto.pcap" "host $IP" 2>/dev/null &
  T=$!
  sleep 0.7

  ans=$(q example.com A "${a[@]}" 2>/dev/null | head -1) || true
  sleep 1.2

  kill "$T" 2>/dev/null || true
  wait "$T" 2>/dev/null || true

  # An unanswered exchange still puts packets on the wire. Without this check a
  # failed transport prints a plausible-looking byte count.
  [ -n "$ans" ] || { printf "%-9s no answer - not counted\n" "$proto"; continue; }

  tshark -r "$D/$proto.pcap" -T fields -e ip.src -e ip.len 2>/dev/null | awk -v ip="$IP" -v p="$proto" \
    'NF==2 { pk++; by+=$2; if ($1==ip) rx+=$2; else tx+=$2 }
     END { if (pk == "") printf "%-9s answered, but nothing seen on %s - wrong address?\n", p, ip
           else printf "%-9s %2d packets %6d bytes   sent %5d   received %5d\n", p, pk, by, tx, rx }'
done

One run on this host, with KEEP=1 so the captures survive:

Interface ens1, resolver 149.112.112.112
----------------------------------------------------------------------
do53       2 packets    168 bytes   sent    68   received   100
dnscrypt   4 packets    988 bytes   sent   423   received   565
dot       21 packets   7218 bytes   sent  2241   received  4977
doh       24 packets   7740 bytes   sent  2565   received  5175
doh3      23 packets  11663 bytes   sent  7072   received  4591
doq       16 packets  11018 bytes   sent  6781   received  4237

captures kept in /tmp/tmp.5LpXThzPPz

Keep the captures whenever a number looks wrong. The counting is deliberately crude - every packet to and from the address, no attempt to tell a handshake from an answer - and the pcap is the only way to see what it actually counted. That is not a hypothetical: an earlier pass here read a server's certificate, split across two TCP segments, as though it were a DNS answer, and the only reason we noticed was that the number looked too tidy.

Two of those checks exist because the measurement lied to us first. An unanswered exchange still puts packets on the wire, so a transport that fails will happily print a plausible byte count - pointed at an address that answers nothing, an early version of this script reported do53 1 packets 68 bytes and dot 7 packets 420 bytes and exited cleanly. And a query can succeed while the capture sees nothing, because the client used a different address than the filter watched. Both now say so instead of printing a number.

A single run of anything is an anecdote, so treat one pass as a shape rather than a number. Expect the TCP and QUIC arms to move by a few hundred bytes between runs and the two UDP arms to sit almost still. Point IP at any resolver that answers more than one protocol and the comparison holds - but change the stamp with it, or the DNSCrypt arm will keep talking to Quad9 while everything else moves.

What we measured

Every comparison above is the sort of thing you can read anywhere. The rest of this page is different: we run all four encrypted transports on one resolver, so we can put the same query through each of them and count what comes back. These numbers are ours, taken on 29 and 30 July 2026.

How the measurements were taken

  • • A client in its own network namespace, connected by a veth pair with a 1500-byte MTU, so packets are segmented the way they are on a real path rather than the way they are on loopback.
  • • A fixed 20 ms delay in each direction, injected with netem on both ends of that veth. A round trip therefore costs 40 ms, which makes round trips visible in the timings instead of having to be inferred. The delay touches nothing but this harness.
  • • One client for every transport (q, Go), so differences are the protocols and not five different implementations. Each sample is a fresh process, which means a cold connection: no session resumption, no 0-RTT.
  • • The same question every time, example.com A, pre-warmed so it is served from cache and the resolver's own work is close to zero.
  • • Byte counts are IP-layer and exclude the Ethernet header. Medians of 9 runs for the packet tests, 300 runs for the loss test.

The harness is public: dns-transport-bench is the namespace setup, the measurement script and the raw output from this run, so the numbers below can be checked or repeated against any other resolver.

Two honest limits. This is one vantage point and one client stack: the round-trip counts, byte counts and loss behaviour are properties of the protocols and travel, but absolute milliseconds describe this path only. And we do not run a DNSCrypt endpoint, so DNSCrypt appears in the tables above but not in the measurements below.

1. How many round trips before the answer

On a path where one round trip costs exactly 40 ms, the time to a first answer on a cold connection reads directly as a round-trip count.

Time to first answer, cold connection lower is better · one round trip = 40 ms 1 RTT 2 RTT 3 RTT 4 RTT Do53 (plaintext): 40.8 ms, 1.0 round trip Do53 40.8 ms plaintext, no handshake DoQ (DNS over QUIC): 105.0 ms, 2.6 round trips DoQ 105.0 ms DoT (DNS over TLS): 143.6 ms, 3.6 round trips DoT 143.6 ms DoH over HTTP/2: 144.9 ms, 3.6 round trips DoH 144.9 ms DoH3 (DNS over HTTP/3): 147.9 ms, 3.7 round trips DoH3 147.9 ms
 Time to answerRound tripsPackets out / in
Do5340.8 ms1.01 / 1
DoQ105.0 ms2.64 / 7
DoT143.6 ms3.65 / 4
DoH144.9 ms3.68 / 4
DoH3147.9 ms3.74 / 7

DoQ reaches an answer a full round trip before DoT and DoH. That is QUIC doing what it was designed to do: the transport handshake and the TLS handshake are the same handshake, where TCP has to complete its own three-way handshake before TLS can start.

The result worth noticing is DoH3, which is also QUIC and yet lands with the TCP transports. The obvious explanation is that HTTP/3 spends the round trip QUIC saves, opening control streams and exchanging settings before the query can go out. We published that explanation, then captured the handshake, and it is not what happens.

The round trip goes to QUIC address validation. Our server runs with quic_retry enabled, so a new connection is answered first with a 119-byte Retry packet carrying a token, and the client must send its entire ClientHello a second time before the handshake proceeds. That is the whole difference: three round trips instead of DoQ's two. It is a setting on this resolver rather than a property of the protocol, so a DoH3 endpoint without address validation would not show the gap at all.

One DoH3 handshake, captured the extra round trip is address validation, not HTTP/3 clientserver 0 ms: the ML-KEM ClientHello does not fit one packet, so the client sends two Initials 0 ms 1 280 B 1 280 B 20 ms: the server answers with a Retry packet instead of a handshake 20 ms 119 B — Retry, with token 40 ms: the client repeats the identical ClientHello, now carrying the token 40 ms the same 2 × 1 280 B again 61 ms: the server sends its entire flight in one burst, with no anti-amplification stall 61 ms 5 189 B, one burst, no stall 104 ms: the client finishes the handshake and sends the DNS query 104 ms Finished + query answer at 125 ms · 3 round trips, one of them spent on Retry

2. Bytes on the wire for one lookup

The same question and the same answer, counted from the first packet of the connection to the last. This is the true cost of a cold lookup, handshake included.

Bytes on the wire, one cold lookup IP layer, handshake included · lower is better client → server server → client Do53: 71 B out, 146 B in, 217 B total Do53 217 B the whole exchange fits in two packets DoT: 1770 B out, 5063 B in, 6833 B total DoT 6 833 B DoH over HTTP/2: 2231 B out, 5007 B in, 7238 B total DoH 7 238 B DoQ: 4019 B out, 5723 B in, 9742 B total DoQ 9 742 B DoH3: 5232 B out, 5623 B in, 10855 B total DoH3 10 855 B Do53 is 217 bytes. The cheapest encrypted transport costs 31 times that, and all of the difference is handshake.
 Client → serverServer → clientTotalvs Do53
Do5371 B146 B217 B
DoT1 770 B5 063 B6 833 B31×
DoH2 231 B5 007 B7 238 B33×
DoQ4 019 B5 723 B9 742 B45×
DoH35 232 B5 623 B10 855 B50×

Around 5 KB of every encrypted lookup is the same thing in all four cases: the server's certificate chain. That is a fixed cost paid once per connection and amortised over every query that follows, which is the entire argument for keeping DNS connections open rather than opening one per lookup.

The QUIC transports send noticeably more from the client. QUIC pads its initial packets to at least 1200 bytes as an anti-amplification measure, so a small ClientHello still costs a full packet. Hold on to that detail, because it produces the strangest result on this page.

3. What happens on a lossy link

The same 40 ms path, with 2 % of packets dropped in each direction: roughly what a weak mobile signal or a congested cafe network looks like. Three hundred cold lookups per transport, because a tail measured on a handful of samples is a single unlucky draw rather than a number.

Latency under 2% packet loss, each direction median → 95th → 99th percentile of 300 cold lookups median 95th 99th 0 300 ms 600 ms 900 ms 1200 ms Do53: median 40.8 ms, 95th 41.0 ms, 99th 41.3 ms, but 12 of 300 lookups returned nothing at all Do53 flat 41 ms, but 12 of 300 lookups simply never came back DoQ: median 105.2 ms, 95th 236.5 ms, 99th 310.3 ms DoQ 310 ms DoH3: median 145.7 ms, 95th 286.7 ms, 99th 329.4 ms, with 15 of 300 lookups returning no answer DoH3 329 ms 15 of 300 returned no answer DoT: median 144.2 ms, 95th 477.9 ms, 99th 1225.8 ms, worst 3379 ms DoT 1226 ms DoH over HTTP/2: median 144.2 ms, 95th 522.2 ms, 99th 1222.1 ms, worst 1572 ms DoH 1222 ms At the median, loss barely registers. The difference is entirely in the tail, and the tail is where users notice.
 Median95th99thWorst seenMeanNo answer
Do5340.8 ms41.0 ms41.3 ms42.9 ms40.8 ms12 of 300
DoQ105.2 ms236.5 ms310.3 ms378.9 ms124.7 ms0 of 300
DoH3145.7 ms286.7 ms329.4 ms494.8 ms163.2 ms15 of 300
DoT144.2 ms477.9 ms1 225.8 ms3 379.1 ms217.1 ms0 of 300
DoH144.2 ms522.2 ms1 222.1 ms1 571.5 ms210.3 ms0 of 300

Half of all lookups are untouched by 2 % loss on every transport: each median sits within a millisecond of what the same transport does on a clean path. Nothing separates them until the tail, and then the split is not gradual. The two TCP transports reach 1.2 seconds at the 99th percentile. The two QUIC transports stop at 310 and 329 ms.

The number that explains it is one second. When TCP loses a packet during a handshake there are no later packets to infer the loss from, so it cannot fast-retransmit; it waits out a retransmission timeout, and RFC 6298 puts the floor on that timeout at one second. The measurement lands on it precisely: DoT's 99th percentile is 1 082 ms above its own median and DoH's is 1 078 ms above its own, two independent transports agreeing to within four milliseconds. That is not a latency distribution, it is a constant.

Lose a second packet and the timer doubles, which is what DoT's worst sample is: 3 379 ms, a median plus one second plus two. QUIC's loss recovery is not bound to that floor, and neither QUIC transport went past 495 ms in 300 lookups.

Plain DNS is the instructive case. It is fast in every sample that arrives, because it has no recovery mechanism at all: a lost packet is not retransmitted, it is a lookup that never returns. Twelve of 300 vanished, close to the roughly 4 % you would predict for a single packet each way at 2 % loss. The encrypted transports all recover; they differ only in how gracefully.

The transport that fails worst is ours, and it turned out to be a bug in nginx

Look again at the last column. DoH3 returned no answer on 15 of its 300 lookups, a worse failure rate than plain unprotected DNS. Its tidy 329 ms tail is partly an illusion created by those missing samples: the lookups that would have been slowest are not in the distribution, because they were abandoned instead. Chasing that number down took the rest of the day and ended somewhere we did not expect.

Every failure is a handshake that never completed. Four controls narrowed it:

  • • Forcing the classical key exchange, which shrinks the handshake and changes nothing else, gave 100 successes out of 100.
  • curl, an entirely different QUIC implementation, fails at the same rate as our Go client. Not a client bug.
  • DoQ uses the same client, the same post-quantum key exchange and the same certificate, and failed zero times in 300. Not QUIC, and not the cryptography.
  • • Both wrong guesses we published along the way, the anti-amplification limit and QUIC address validation, were tested and eliminated. Turning address validation off saved a round trip and left the failures exactly where they were.

What was left was the server, so we captured a failing handshake and decrypted it. QUIC's Initial packets can be read without any key material, because their keys derive from the connection ID, and that is precisely the part that matters here.

The post-quantum ServerHello does not fit in one packet. Its ML-KEM key share is 1 088 bytes, which pushes the ServerHello to about 1 178 and splits it across two Initial packets. Lose one of the two and the client holds half a ServerHello: it cannot derive handshake keys, so every packet that follows is undecryptable to it. That much is just packet loss, and recovering from it is routine.

nginx does not recover from it. Its own debug log shows the server detecting the loss and deciding to retransmit, and then never putting the data back on the wire:

frame tx init:1  CRYPTO len:1124 off:0      ← ServerHello, part 1
frame tx init:2  CRYPTO len:54   off:1124   ← part 2
frame rx init:4  ACK n:1 delay:0 2 0        ← client acks 2 and 0, never 1
detect_lost pnum:1 ... level:0          ← nginx sees the loss
resend packet pnum:1                    ← and decides to resend it
frame tx init:3  ACK only
frame tx init:4  ACK only
frame tx init:5..10  ACK only, every one
frame tx init:11 CONNECTION_CLOSE

The CRYPTO frame is sent once and never again. Eight further Initial packets carry nothing but acknowledgements, while every probe timer fires against the Handshake encryption level even though the missing data sits in the Initial one. The client meanwhile does everything right: it acknowledges what arrived, repeats its own ClientHello, and backs off exponentially until it gives up. Two correct participants, one of them withholding the single thing the other needs.

It reproduces on a completely stock nginx, built from unmodified source with a twenty-line configuration, at the same rate. It needs one more ingredient we nearly missed: a certificate chain long enough to span several Handshake packets. With a single small self-signed certificate it did not appear once in 150 attempts, which would have made for an embarrassing bug report.

We have reported it upstream as nginx/nginx#1616, with the packet captures, the debug log and a scripted reproduction published at nginx-quic-initial-repro. The full trace - the capture, the two lines of source that cause it, and the candidate fixes proposed upstream - is in the post-mortem. Until it is fixed, read DoH3's 329 ms as "when it answers", and note that the priority-1 record we publish for discovery offers HTTP/2 alongside HTTP/3, so a client that cannot complete the QUIC handshake falls back to TCP rather than failing. If you run nginx with HTTP/3 and post-quantum key exchange, this affects you too.

4. What the post-quantum handshake costs

All four transports here negotiate X25519MLKEM768, a key exchange designed to stay secure against a future quantum computer. The same ML-KEM-768 is now standard in SSH too, where OpenSSH 10.1 warns when a session falls back to a classical exchange. It is bigger than the classical one. Running the same client twice, once with the post-quantum group and once with classical X25519 forced, prices it exactly.

Bytes for one cold lookup: classical vs post-quantum same client, same transport, key exchange group forced classical X25519 + ML-KEM 768 DoT: 4749 B classical, 7065 B post-quantum, +2316 B DoT 4 749 B 7 065 B +2 316 B DoH over HTTP/2: 4469 B classical, 7237 B post-quantum, +2768 B DoH 4 469 B 7 237 B +2 768 B DoQ: 6985 B classical, 9853 B post-quantum, +2868 B DoQ 6 985 B 9 853 B +2 868 B DoH3: 8486 B classical, 12416 B post-quantum, +3930 B DoH3 8 486 B 12 416 B +3 930 B
 ClassicalPost-quantumCostTime change
DoT4 749 B7 065 B+2 316 Bnone
DoH4 469 B7 237 B+2 768 Bnone
DoQ6 985 B9 853 B+2 868 B40 ms faster
DoH38 486 B12 416 B+3 930 Bnone

Between 2.3 and 3.9 KB, once per connection, and on three of the four transports it costs no measurable time at all: the extra bytes ride in packets that were being sent anyway. Then there is DoQ, where the post-quantum handshake came out a full round trip faster than the classical one. That is not a measurement error.

Why the bigger handshake is the faster one

A QUIC server may not send more than three times the bytes it has received from an address it has not yet validated. It is the rule that stops QUIC servers being used as reflection amplifiers. With the classical key exchange our server has 4 131 bytes to send, and that rule decides whether it may.

The same DoQ handshake, captured twice Classical X25519 first flight 1 308 B → budget 3 × 1 308 = 3 924 B clientserver 0 ms: the client sends one 1308-byte Initial packet 0 ms 1 308 B 21 ms: the server sends 3 x 1308 B and stops, exactly at the anti-amplification limit 21 ms 1 308 B 1 308 B 1 308 B budget spent at 3 924 B the server is not allowed to continue 61 ms: the client's second flight raises the budget 61 ms 1 308 B 82 ms: the rest of the handshake, then the answer 82 ms 207 B answer at 152 ms · 4 round trips With ML-KEM 768 first flight 2 616 B → budget 3 × 2 616 = 7 848 B clientserver 0 ms: the larger ClientHello does not fit in one packet, so the client sends two Initials 0 ms 1 308 B 1 308 B 21 ms: the server sends its whole 5278-byte flight at once, well inside the larger budget 21 ms 1 308 B 1 308 B 1 308 B 1 354 B 5 278 B sent in one flight, no stall 63 ms: the client finishes the handshake and sends the DNS query 63 ms 1 403 B answer at 105 ms · 3 round trips

With the classical key exchange the ClientHello fits in one packet, so the client's first flight is 1 308 bytes and the server may answer with at most 3 924. It has 4 131 bytes to send. It sends exactly 3 × 1 308 bytes, stops 207 bytes short of finishing, and waits for the client to speak again before it is allowed to send them. That wait is the extra round trip, and the capture shows those 207 bytes arriving 21 ms after the client's next packet.

The post-quantum ClientHello is too large for one packet. The client sends two, so the budget doubles to 7 848 bytes. The server's flight is larger too, 5 278 bytes rather than 4 131, because the key share it carries is bigger - and it still fits with room to spare. The handshake completes a round trip sooner. The post-quantum key exchange pays for itself by lifting the client over the limit.

The lever underneath is handshake size, not cryptography. A server whose flight fits inside 3 924 bytes never stalls in the first place, which is a concrete argument for short certificate chains, ECDSA rather than RSA, and no unnecessary intermediates. It also means the result is ours rather than universal: on a resolver with a smaller certificate chain, classical DoQ would not stall, and this asymmetry would not appear.

What the numbers change. Not much about privacy: all four encrypt the lookup, and the choice between them was never a privacy choice. What they do settle is the performance question that the tables above can only gesture at. DoQ is genuinely the quickest to a first answer and the steadiest when packets go missing. DoH3 inherits QUIC's loss behaviour but hands QUIC's handshake advantage back to address validation, which is our setting to change rather than a fact about the protocol. DoT and DoH are indistinguishable at the median and differ only in their tails, where DoH is the more exposed of the two.

And every one of these differences is a cold-connection difference. A resolver connection that stays open turns all of this into a single first lookup, after which every transport costs one round trip and a couple of hundred bytes. Which is the strongest practical argument on this page: whichever you pick, pick the one your device will keep open.

These measurements are published under CC BY 4.0. Reuse the numbers, the tables and the charts freely, including commercially, as long as you credit dnsdoh.art and link back to this page.

What actually separates them

Privacy from the network is a tie. They all hide the lookup contents from everyone between you and the resolver. If that is your only goal, any of them does the job.

Privacy from the resolver is where they part. By default the resolver you connect to still sees every query, whichever protocol you used. The practical way to break that link is a relay: the client sends each query through a relay so the resolver answering it never learns your address. Anonymized DNSCrypt does exactly this, set up in dnscrypt-proxy. Oblivious DoH was the standardized attempt at the same idea but never saw real deployment, so for now relays are effectively DNSCrypt's domain.

Resistance to blocking follows the port. DoH, DoH3, and DNSCrypt sit on 443 alongside normal web traffic and are hard to isolate. DoT and DoQ sit on 853, which a network can block outright when it wants to force DNS back into the open.

Performance and reach favour different winners. DoQ and DoH3 handle packet loss best and connect fastest; DoT and DoH are well supported and perfectly fast on stable links; DoH and DoH3 are the ones that work inside a browser with no system changes; DNSCrypt needs its own client but brings relay anonymity the others lack natively.

Which should you use

If you only want the short answer, here is how the five sort out on the questions people actually ask. Each verdict assumes the resolver and the client both support the protocol.

Fastest

DoQ & DoH3

Both ride QUIC, so they finish their handshake in fewer round trips and a single lost packet never holds up the ones behind it. The lead is widest on mobile and other lossy links; on a stable wired connection the rest are close behind.

Most stable & compatible

DoT & DoH

The mature pair. DoT is the native standard on Android and Linux; DoH is built into every major browser and into Windows 11. If you want something that simply works on the widest range of devices today, start with these two. Recent systems can also find them on their own: Windows 11 and Apple platforms use automatic discovery to upgrade a plain resolver to its encrypted equivalent.

Hardest to block

DoH & DoH3

Both sit on port 443 next to ordinary web traffic, so a network cannot single them out without breaking the rest of the web. On a restricted or filtered connection this is usually the deciding factor. DNSCrypt on 443 is a close third.

Easiest to turn on

DoH & DoT

DoH is a single toggle in Chrome, Edge, Firefox, and Windows 11. DoT is one hostname field in Android's Private DNS. Neither needs an extra app or a configuration file, unlike DNSCrypt, which needs its own client.

Best for source privacy

DNSCrypt

Anonymized DNSCrypt routes your query through a relay (configured in dnscrypt-proxy), so the resolver that answers never sees who asked. Oblivious DoH aimed to bring the same split to DoH but never really shipped, which leaves DNSCrypt's relays the one option you can actually use today.

Best all-round

DoH3

Where it is supported it gives you everything that matters at once: the port-443 blending that resists blocking, QUIC's speed and loss recovery, and browser support with no extra software. If your resolver and client both speak it, it is the strongest default. It is the form this service runs natively.

What matters more than the protocol

Once the lookup is encrypted, the larger questions are who answers it and whether your setup actually uses the encrypted path. A protocol you trust pointed at a resolver you do not is a poor trade, and an encrypted resolver that your device quietly bypasses protects nothing.

This service answers over DNS over HTTPS, DNS over TLS, and HTTP/3; the protocols page lists what is offered and the setup guide has the per-platform steps. After you configure any of them, confirm it took effect: how the DNS leak test works explains the check, and you can run the leak test to see which resolver actually answered.

Pick one and confirm it works

Configure an encrypted protocol, then verify the lookups really travel it.

Ozy-666 Author · Creator of dnsdoh.art

Ozy-666 builds and operates dnsdoh.art, an encrypted DNS resolver serving DoH, DoH3, DoT and DoQ. He maintains its Edge-optimised AdGuardHome and dnscrypt-proxy forks and the custom resolver, TLS and firewall stack these guides document. They are written from running that infrastructure day to day.