Skip to content

From Cloudflare's network

Flarebox’s Post-quantum page checks a server from your computer. With From Cloudflare’s network on (Settings → Features), it can also check from a Cloudflare Worker, or both at once. That’s the vantage point that matters for an origin: the Worker connects to it the way Cloudflare’s edge does.

Both shows the two verdicts side by side. When they disagree, the server answers differently depending on where it’s asked from. That’s typical of a provider rolling ML-KEM out to some of its servers or regions at a time. Check again; the fleet’s host history shows whether it flaps.

By default, the public checker on pqdoctor.com, the one behind Try it. Deploy your own (in the Post-quantum page’s Cloudflare’s network card) puts the checker on your account as a reviewed change: pick the account and a Worker name (pqdoctor-check by default). The write token needs Account → Workers Scripts → Edit. Flarebox then uses its workers.dev address, and Use the public one goes back.

Either way the result isn’t kept in the history, which is your computer’s own checks.

The checker (pqdoctor web) is the same Rust engine as the CLI and Flarebox, compiled to WebAssembly (about 200 KB, a plain C ABI with JSON in and out). TypeScript only does the I/O over cloudflare:sockets (connect() with secureTransport: "off"): it sends pqdoctor’s seven ClientHellos one at a time and reads only the server’s first flight. Its probe loop mirrors the Rust one: 5 s connect and read timeouts, 120 ms between probes, one retry for a silent probe, and stop if nothing is listening.

The ML-DSA check runs there too. The TLS 1.3 handshake is a state machine in the WebAssembly engine: the Worker gets a ClientHello for a client key made from crypto.getRandomValues, writes it to the socket, and feeds back every byte it reads until the engine decides. The X25519 exchange and key schedule run in WebAssembly, and Finished is never sent.

Checked against Cloudflare’s TCP sockets and limits docs on 2026-10-13:

  • No sockets to Cloudflare’s own addresses, localhost or private networks, and none on port 25. So the Worker resolves the name first, over DNS over HTTPS. A name on Cloudflare’s published ranges is reported as served by Cloudflare, whose edge already negotiates X25519MLKEM768 with browsers; what’s left to check is the origin behind it. Private or reserved addresses are refused.
  • At most 6 connections waiting for a response at once per invocation; a check uses one socket at a time.
  • Subrequests: 50 per invocation on Free, 10,000 on Paid. A check needs at most about 19: up to 16 sockets (7 probes, up to 7 retries, 2 certificate probes), 2 DNS lookups and 1 HEAD.

On top of those: hostnames only (no IP literals, single-label or reserved names), 10 checks per minute per client IP, and results cached for 10 minutes per host and port. Failures aren’t cached.

Any port works if it speaks TLS from the first byte: 443 by default, or example.com:8443 in the box (465, 993, 995, 853, 636, 2053 and so on). STARTTLS ports are refused before anything is sent, because the Worker would have to speak the plaintext protocol first: 21, 25, 110, 143, 389, 587, 3306, 3389, 5222 and 5432 (25 is blocked by the platform anyway). Check those from this computer, or with pqdoctor host:port. The same rules apply on every port: public addresses only, and a name on Cloudflare’s ranges is reported as served by Cloudflare without probing. The Server header (one plain HTTP request to port 80) is only fetched for port 443.

Locally (wrangler 4.145 / workerd, and Node): the WebAssembly engine gives the same report as the CLI (verdict, cause, headline, fixes and every probe summary) on post-quantum, classical and PQ-codepoint-resetting test servers. The probe loop over real sockets agrees with the CLI on 6 of 6 real hosts, and the ML-DSA section matches the CLI’s exactly on real OpenSSL 3.5 servers.

On Cloudflare’s network (2026-10-16): the same verdict and cause as the CLI for github.com (no-mlkem), nginx.org and www.mit.edu (tls12-only), www.debian.org (no-mlkem) and www.northeastern.edu (post-quantum). The ML-DSA section reads github.com’s chain; cloudflare.com is reported as served by Cloudflare; IP addresses and localhost are refused; and the rate limiter answers 429 in a burst (approximately, since Workers Rate Limiting counts per location).

Still open:

  • How production egress treats the split-hello probes. The Worker writes the two halves 40 ms apart, but whether they leave as separate TCP segments can’t be observed from the Worker, so the pq-split-dropped warning may not reproduce.
  • Whether some origins answer Cloudflare’s egress differently (rate limits, geo or bot rules) from a residential IP, across many hosts.