Post-quantum DNSSEC makes DNS over TCP mandatory
Post-quantum DNSSEC answers signed with ML-DSA-44 do not fit in UDP. The authoritative servers for today's test zones truncate every DNSSEC answer, so TCP/53 is the only working path. The cost lands on recursive resolvers, including ones that cannot validate algorithm 18.Post-quantum DNSSEC makes DNS over TCP mandatory: with ML-DSA-44 (DNSSEC algorithm 18) signatures, the authoritative servers for today's test zones truncate every DNSSEC answer sent over UDP, so TCP is the only path that returns any data. A recursive resolver that cannot open TCP connections to port 53 cannot resolve a post-quantum signed name at all.
Everything below was measured at the time of writing with dig 9.10.6 from macOS, against Cloudflare's dnstest.dev test zones and Shumon Huque's mldsa.huque.com. The test records carry a TTL of 0 and RRSIGs valid for about 25 hours, and the operators can change them at any time, so rerun the commands before relying on any number here. The dig output below is trimmed to the relevant lines, with long base64 keys and signatures cut at ....
How big a post-quantum DNSSEC answer is
The signature is the problem. Cloudflare's 10 September 2026 announcement of ML-DSA-44 validation on 1.1.1.1 puts it at 2,420 bytes, "almost 38 times larger" than an ECDSA P-256 signature. The key sizes come from the same table, from draft-westerbaan-dnssec-mldsa-04 for ML-DSA-44, and from RFC 8080 for Ed25519. IANA lists algorithm 18 as ML-DSA-44 in the DNSSEC algorithm numbers registry, referencing the draft.
| Algorithm | Number | Public key | Signature |
|---|---|---|---|
| RSA-2048/SHA-256 | 8 | 260 bytes | 256 bytes |
| ECDSA P-256 | 13 | 64 bytes | 64 bytes |
| Ed25519 | 15 | 32 bytes | 64 bytes |
| ML-DSA-44 | 18 | 1,312 bytes | 2,420 bytes |
Raw sizes undersell it, because a DNSKEY answer carries the key and a signature over it. These are the full DNS message sizes dig reported for the test zones, fetched over TCP straight from the authoritative servers, with cloudflare.com (algorithm 13) as the classic baseline:
| Query | Signing | Message size |
|---|---|---|
mldsa44.dnstest.dev DNSKEY |
ML-DSA-44 only | 3,847 bytes |
valid.mldsa44.dnstest.dev DNSKEY |
ML-DSA-44 only | 3,859 bytes |
upgrade.mldsa44.dnstest.dev DNSKEY |
ML-DSA-44 + P-256 | 4,066 bytes |
mldsa.huque.com DNSKEY |
ML-DSA-44 only | 3,839 bytes |
valid.mldsa44.dnstest.dev A |
ML-DSA-44 only | 2,563 bytes |
upgrade.mldsa44.dnstest.dev A |
ML-DSA-44 + P-256 | 2,690 bytes |
cloudflare.com DNSKEY |
P-256 | 313 bytes |
cloudflare.com A |
P-256 | 185 bytes |
For scale: during the root KSK rollover, the root DNSKEY response reached 1,414 octets with both keys published, and people worried in advance that it would break resolvers that could not receive large UDP messages or use TCP. RFC 9210 section 5.2 records that no problems were reported. A post-quantum zone apex DNSKEY answer is more than two and a half times that size, and even a single A record answer is bigger.
The authoritative truncates even when you offer 4,096 bytes
The usual story is "big answer, TC bit, retry over TCP", with TCP as the occasional fallback. On the post-quantum test zones it is not occasional. Here is a DNSKEY query sent straight to one of Cloudflare's authoritative servers for mldsa44.dnstest.dev, advertising a 4,096-byte buffer, with +ignore so dig shows the UDP reply instead of silently retrying:
$ dig @jake.ns.cloudflare.com mldsa44.dnstest.dev DNSKEY +dnssec +norec +bufsize=4096 +ignore
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 44305
;; flags: qr aa tc; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1
; EDNS: version: 0, flags: do; udp: 1232
;; MSG SIZE rcvd: 48The full answer is 3,847 bytes, which fits inside the 4,096 bytes the query offered. The server still sent back an empty, 48-byte truncated reply, and its own OPT record advertises udp: 1232. The same query with +tcp returns the whole RRset:
$ dig @jake.ns.cloudflare.com mldsa44.dnstest.dev DNSKEY +dnssec +norec +tcp
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 51683
;; flags: qr aa; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
mldsa44.dnstest.dev. 0 IN DNSKEY 257 3 18 uC68c2pp+1wz6C219hTCxrHLoTQS3/D7i4ml...
mldsa44.dnstest.dev. 0 IN RRSIG DNSKEY 18 3 0 20261005193818 20261004183818 25195 mldsa44.dnstest.dev. sKJlShVzfx...
;; MSG SIZE rcvd: 3847The A query for valid.mldsa44.dnstest.dev behaved the same way: a 54-byte truncated UDP reply at +bufsize=4096, the full 2,563 bytes only over TCP. Both of the zone's nameservers (jake and lisa) did this, over IPv4 and IPv6. Without the DO bit, the same A query came back over UDP in 86 bytes, because no RRSIG is attached.
This is not a Cloudflare quirk. Shumon Huque's zone runs on a different operator's server and does the same:
$ dig @guvnor.huque.com mldsa.huque.com DNSKEY +dnssec +norec +bufsize=4096 +ignore
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 44050
;; flags: qr aa tc; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1
; EDNS: version: 0, flags: do; udp: 1232
;; MSG SIZE rcvd: 44Over TCP that answer is 3,839 bytes. Even the SOA answer for mldsa.huque.com (2,572 bytes over TCP) came back truncated over UDP.
A caveat on what this shows. We observed a 1,232-byte UDP ceiling at two independent authoritative servers; neither operator documents it as a policy that we could find, so treat it as observed behaviour, not a promise. It also barely matters. RFC 9715 (January 2025) recommends a maximum DNS/UDP payload of 1,400 bytes, and its implementation appendix lists 1,232 as the default buffer for BIND 9, Knot, PowerDNS and Unbound, in line with DNS Flag Day 2020. Every answer in the size table above except the two cloudflare.com rows is bigger than either limit. Whichever side sets the ceiling, a post-quantum signed answer does not fit.
Why your laptop won't notice, but your resolver will
A plain lookup from a client stays small. Without +dnssec, the stub query through 1.1.1.1 never sees the signature:
$ dig @1.1.1.1 valid.mldsa44.dnstest.dev A
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1
valid.mldsa44.dnstest.dev. 0 IN A 188.114.96.0
valid.mldsa44.dnstest.dev. 0 IN A 188.114.97.0
;; MSG SIZE rcvd: 86The ad flag says 1.1.1.1 validated the ML-DSA-44 chain, but the 2,420-byte signature stayed on the resolver's side. Ask for the DNSSEC records and the client gets the same treatment the resolver got upstream:
$ dig @1.1.1.1 valid.mldsa44.dnstest.dev A +dnssec
;; Truncated, retrying in TCP mode.
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1
; EDNS: version: 0, flags: do; udp: 1232
valid.mldsa44.dnstest.dev. 0 IN RRSIG A 18 4 0 20261005193806 20261004183806 23176 valid.mldsa44.dnstest.dev. Aii1MKPnEr...
;; MSG SIZE rcvd: 2563A validating resolver has no such choice. It needs the RRSIG and DNSKEY records to validate, so it sets DO on every upstream query into a signed zone, and for a post-quantum zone each of those queries gets truncated. That is where the TCP requirement lands: on whoever runs the recursive resolver. For a public resolver user that is someone else's problem. For an enterprise BIND or Unbound, or a Pi-hole forwarding to its own local Unbound, it is yours.
Cloudflare's post says as much in one sentence: its figure that around 85% of queries reach 1.1.1.1 over UDP "describe how queries reach Cloudflare's resolver services, not how 1.1.1.1 communicates with authoritative servers." The resolver-to-authoritative leg is the one that changes.
Resolvers that can't validate algorithm 18 still pay the TCP cost
The surprise in the measurements is who pays. Here is the result of dig @<resolver> <zone>.dnstest.dev A +dnssec against four public resolvers, run once per cell at the time of writing (the Google SERVFAIL was repeated three more times, plus against 8.8.4.4, with the same result):
| Zone | 1.1.1.1 | 8.8.8.8 | 9.9.9.9 | 208.67.222.222 |
|---|---|---|---|---|
valid.mldsa44 (ML-DSA-44 only, valid) |
NOERROR, ad | NOERROR, no ad | NOERROR, no ad | NOERROR, no ad |
invalid.mldsa44 (ML-DSA-44 only, broken) |
SERVFAIL | NOERROR | NOERROR | NOERROR |
downgrade.mldsa44 (broken ML-DSA-44, valid P-256) |
SERVFAIL | NOERROR | NOERROR | NOERROR |
upgrade.mldsa44 (valid ML-DSA-44, broken P-256) |
NOERROR, ad | SERVFAIL | NOERROR | NOERROR |
invalid.alg13 (P-256 only, broken; control) |
SERVFAIL | SERVFAIL | SERVFAIL | SERVFAIL |
valid.alg13 (P-256 only, valid; control) |
NOERROR, ad | NOERROR, ad | NOERROR, ad | NOERROR, ad |
The control rows show all four resolvers validate DNSSEC. Only 1.1.1.1 validates algorithm 18. The other three return valid.mldsa44 and even the deliberately broken invalid.mldsa44 without the ad flag, which is what RFC 4035 section 5.2 prescribes: if a validator supports none of the algorithms in an authenticated DS RRset, it treats the child as if no DS existed, so the zone is insecure, not bogus. The parent mldsa44.dnstest.dev publishes only an algorithm 18 DS, so the whole subtree drops to insecure for them.
Look at the sizes, though. Google, Quad9 and OpenDNS all returned the full 2,563-byte answer for valid.mldsa44, RRSIG included. They asked the authoritative with DO set, and since both of its nameservers answered every UDP DNSSEC query we sent with TC, they almost certainly fetched it over TCP. They paid the full post-quantum transport cost and got no validation out of it. For that query Google and Quad9 advertised udp: 512 back to dig, so the client leg was truncated and retried over TCP as well. OpenDNS went the other way: dig advertises 4,096 bytes by default and OpenDNS sent the 2,563-byte answer back over UDP, which means an IP-fragmented datagram. With +bufsize=1232 it truncated instead.
The 8.8.8.8 SERVFAIL on the upgrade zone
One cell does not fit the pattern. The upgrade zone has a valid ML-DSA-44 path and deliberately broken P-256 signatures, and Cloudflare's dnstest.dev resolver test page says a resolver without algorithm 18 support "may instead treat the entire subtree as insecure." Quad9 and OpenDNS did that. Google returned SERVFAIL with this Extended DNS Error (code 6, DNSSEC Bogus):
$ dig @8.8.8.8 upgrade.mldsa44.dnstest.dev A +dnssec
;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 28837
; OPT=15: 00 06 ... ("..RRSIG with malformed signature found for upgrade.mldsa44.dnstest.dev/a (keytag=48671)")
;; MSG SIZE rcvd: 147Key tag 48671 is the zone's algorithm 13 key; upgrade.mldsa44.dnstest.dev publishes DS records for both 48671 (algorithm 13) and 26345 (algorithm 18). The same query with +cd returns the 2,690-byte answer, and the query without +dnssec also returns SERVFAIL. We are reporting what we saw, not why: we have not found a public explanation, and the zone exists to probe exactly this kind of edge. The practical note is that a dual-signed zone during migration exposes differences between validators that single-algorithm zones never do, and a plain stub client on 8.8.8.8 sees that difference as a failed lookup.
The downgrade row is the deliberate one. RFC 6840 section 5.11 says validators "SHOULD accept any single valid path". Cloudflare's post explains that 1.1.1.1 overrides that as local policy: if the authenticated DS RRset contains a supported post-quantum algorithm, at least one valid post-quantum path is required, so a forged P-256-only answer cannot downgrade the zone.
What happens when TCP/53 is blocked
TCP for DNS is not a new requirement. RFC 9210 (BCP 235, March 2022) states it flatly:
All DNS resolvers and servers MUST support and service both UDP and TCP queries.
The same document says network operators "MUST allow DNS service over both UDP and TCP transports", and calls out the old belief that TCP is only for zone transfers, with "filtering all DNS-over-TCP traffic even described as a best practice." DNS Flag Day 2020 told operators to check their firewalls because "some of them block TCP/53." We could not find a current measurement of how many networks still do; the specs and the Flag Day text treat it as common enough to warn about, and that is as far as the evidence goes.
Until now a blocked TCP/53 egress was mostly invisible, because almost every answer fit in UDP. Hypothetical, but concrete: suppose an office resolver runs Unbound with validation on, behind a firewall rule that allows outbound UDP/53 and nothing else on port 53. Classic zones keep resolving, because answers like cloudflare.com's 313-byte DNSKEY RRset stay far below the UDP limit. The first time a zone it depends on signs with ML-DSA-44, the resolver gets a TC reply, opens a TCP connection, and waits for a handshake that never completes. From the outside, that looks like this:
$ dig @192.0.2.1 mldsa44.dnstest.dev DNSKEY +dnssec +norec +tcp +time=3 +tries=1
;; connection timed out; no servers could be reached(192.0.2.1 is a documentation address that never answers, which is what a silently dropped TCP SYN looks like to the resolver.) Clients behind that resolver see slow lookups ending in SERVFAIL or a timeout, depending on the resolver software, for that zone only. RFC 9210 section 5.1 describes the mirror case, an authoritative that blocks TCP, where pending connections "consume resources on the resolver until they time out." Unlike the chain-of-trust breaks covered in diagnosing DNSSEC validation failures behind SERVFAIL, nothing in the zone is wrong. dig +cd through the same resolver will not help either, because the data never arrives in the first place.
Our position: a network that blocks TCP/53 for its recursive resolvers already has a latent outage. It is waiting for a zone that matters to start signing with algorithm 18, and Cloudflare's stated next step is ML-DSA-44 signing in Cloudflare Authoritative DNS, with DS support in Cloudflare Registrar, free for all customers. Fix the egress rule now, while the only affected names are test zones.
How to check your resolver path today
Run these from the host that does the recursion, not from a laptop that forwards to it.
1. TCP/53 egress to an authoritative. This should print a DNSKEY answer of about 3,800 bytes. A timeout means the path blocks DNS over TCP.
dig @jake.ns.cloudflare.com mldsa44.dnstest.dev DNSKEY +dnssec +norec +tcp2. The truncation, for comparison. Expect flags: qr aa tc and a 40 to 60 byte reply, which is everything UDP will give you.
dig @jake.ns.cloudflare.com mldsa44.dnstest.dev DNSKEY +dnssec +norec +bufsize=4096 +ignore3. The full path through your resolver. Replace 10.0.0.53 with your resolver's address. NOERROR with a 2,563-byte answer means the resolver reached the data over TCP. A SERVFAIL or timeout on the first query while the valid.alg13 control resolves points at transport, not DNSSEC.
dig @10.0.0.53 valid.mldsa44.dnstest.dev A +dnssec
dig @10.0.0.53 valid.alg13.dnstest.dev A +dnssec4. Whether it validates algorithm 18 at all. SERVFAIL on both queries means it does. NOERROR without ad means it treats post-quantum zones as insecure, which is what the three non-Cloudflare public resolvers in our matrix did.
dig @10.0.0.53 invalid.mldsa44.dnstest.dev A +dnssec
dig @10.0.0.53 downgrade.mldsa44.dnstest.dev A +dnssecOne more check, on the tooling side. RFC 9210 section 6 says operators "SHOULD ensure that their monitoring and logging applications properly capture DNS messages over TCP." If your DNS query logging or packet capture only watches UDP/53, the post-quantum part of your resolver traffic is invisible to it.
What post-quantum DNSSEC doesn't solve yet
Validator support is thin. The community ML-DSA-44 implementation tracker, last updated 29 September 2026, lists these validating resolvers:
- Cloudflare 1.1.1.1
- PowerDNS Recursor (on master, expected in 5.5.0)
- Loop (1.99.22 and later)
- dnspython and Hark (on master)
BIND, Unbound and Knot Resolver are not on it. Our matrix matches: of the four big public resolvers we tested, only 1.1.1.1 validates algorithm 18.
Dual signing is the migration path, and it is the worst of both for now. The upgrade zone's DNSKEY answer is 4,066 bytes because it carries both key sets and both signatures. Validators that only understand P-256 get nothing from the ML-DSA-44 half, yet every resolver that sets DO hauls it over TCP. Cloudflare's post notes that key rollovers add more keys on top. Proposals to shrink the signatures, such as ML-DSA with Merkle Tree Ladders, are individual Internet-Drafts that no working group has adopted, so plan for the full 2,420 bytes.
Cloudflare calls DNS over transports other than UDP "already a normal part of operating 1.1.1.1 at scale." For 1.1.1.1 that is true. For a resolver behind a firewall rule written when TCP was "only for zone transfers", post-quantum DNSSEC is the change that turns that rule into an outage.