Scanning a list
pqdoctor scan checks a list of hosts and writes one result per host. It’s what the origin study used for 5,139 servers.
One target per line, in any of these forms, mixed freely:
example.comapi.example.com:8443[2001:db8::1]:443https://shop.example.com/checkout1,google.comhostorhost:port. The port defaults to 443, or--port.[v6addr]:portfor IPv6 addresses.- An
https://URL: the host and port are taken from it. - A Tranco list line (
rank,domain): the last field is used.
- reads standard input: cat hosts.txt | pqdoctor scan -. Lines that don’t parse are counted as invalid and skipped.
Output
Section titled “Output”On screen you get tab-separated rows on stdout, and a start line, progress and a summary on stderr:
host port verdict cause group stack headlinegithub.com 443 notPq no-mlkem - - Not post-quantumcloudflare.com 443 pq pq-preferred X25519MLKEM768 cloudflare Post-quantum: negotiates X25519MLKEM768nginx.org 443 noTls13 tls12-only - nginx Not post-quantum: TLS 1.2 onlypqdoctor: 3 targets (0 invalid lines, 0 excluded, 0 already done) · 3 at a time · ≤2 new/spqdoctor: 3 targets, 0 skipped, 3 answered TLS, 1 post-quantum (33.3% of answered) no-mlkem 1 pq-preferred 1 tls12-only 1For files, use JSON Lines:
pqdoctor scan hosts.txt --json --out results.jsonl--out always appends JSON records, whatever’s on screen. Results are written as hosts finish, not in input order. The fields are in JSON output.
Pacing
Section titled “Pacing”| Option | Default | Max |
|---|---|---|
--concurrency N (-c) |
4 hosts at once | 32 |
--rate R |
2 new hosts a second | 20 |
Each host’s seven probes always run one at a time, 120 ms apart. Parallel probes against one server trip its rate limiter and give wrong answers, which is how the one-at-a-time rule came about.
At the defaults, a scan does roughly 2 hosts a second. The study’s 5,139 hosts took about 80 minutes at 4 at a time and 2 a second.
Resuming
Section titled “Resuming”pqdoctor scan big-list.txt --out results.jsonl --resume--resume skips every host and port already in --out, so an interrupted scan picks up where it stopped. It matches on the probed host, so --www-fallback results resume correctly too.
Being a good neighbour
Section titled “Being a good neighbour”A scan touches servers you don’t own. Make it easy for their owners to see what it is and to opt out.
- Identify yourself.
--contactadds an email or URL to the User-Agent of the one HTTP request each host gets:Terminal window pqdoctor scan hosts.txt --contact security@example.orgUser-Agent: pqdoctor/0.2.0 (post-quantum TLS check; handshake-only; contact: security@example.org) - Honour opt-outs. Keep an exclude file and pass it to every scan. A domain covers its subdomains:
exclude.txt example.netinternal.example.orgTerminal window pqdoctor scan hosts.txt --exclude exclude.txt - Keep the defaults unless you have a reason. They were chosen so no server sees a burst.
--no-httpskips the port-80 request entirely, if you don’t want the stack-specific fixes.
Skipping CDNs
Section titled “Skipping CDNs”To measure origins rather than CDN edges, skip addresses in CDN networks:
cloudflare 104.16.0.0/13cloudflare 2606:4700::/32fastly 151.101.0.0/16pqdoctor scan hosts.txt --skip-ranges cdn-ranges.tsvHosts that resolve into a listed network are recorded as skipped, with cause cdn-<provider>, and never contacted. The study built its file from each provider’s published list; see Method.
--resolve-only does just the DNS and filtering, with no connections, which is useful for sizing a scan first.
Bare domains
Section titled “Bare domains”Lists like Tranco rank registrable domains, which often have no address of their own. --www-fallback tries www.<domain> when the domain doesn’t resolve. The record’s host is the name probed, and listed keeps the original entry.
Certificates in scans
Section titled “Certificates in scans”Scans skip the ML-DSA check by default, because it costs two more connections a host. --auth turns it on.
Exit status
Section titled “Exit status”scan exits 0 when it finishes, whatever the verdicts, and 2 when it can’t read the input or write --out. To act on results, read them: see Scripting with JSON.