Composed protocol tracks

Measured inside the handshake, not beside it.

Cycle counts on a primitive don’t tell you what a connection costs. These are full TLS and SSH key exchanges — decomposed phase by phase, sized to the byte, and cross-checked against the liboqs speed tools and eBACS reference cycles so they can be verified against the canonical numbers. Measured on x86 and ARM so the figures travel.

Run date
2026-09-21
Commit
Host
Xeon Platinum 8488C
liboqs
0.15.0
Architectures
aarch64 · x86_64
OpenSSL
3.0.13
Every track

Hybrid and pure post-quantum against the classical baseline.

Expand any suite for its phase decomposition. The wire column is fixed by the protocol; the timing columns move with host load — read them as a distribution, and read the tail column as how far it strays.

MLKEM768vs X2551983.3 µs82.1 µs89.0 µs95.0 µs2.77×11,9982,272 B−47.5%one run · 11 Jul 2026 · 9f57fb31 run on an unidentified host — too few for a range
X25519classical baseline157.6 µs156.4 µs165.2 µs175.3 µs1.43×6,34564 Bbaseline
SecP256r1MLKEM768vs X25519235.4 µs233.1 µs243.9 µs250.5 µs2.08×4,2482,402 B+49.0%one run · 11 Jul 2026 · 9f57fb31 run on an unidentified host — too few for a range
X25519MLKEM768vs X25519240.5 µs238.4 µs249.8 µs255.9 µs1.81×4,1572,336 B+52.4%one run · 11 Jul 2026 · 9f57fb31 run on an unidentified host — too few for a range

Timing comparisons are published as a median and range across runs on one machine. The current host, which the record does not identify, has 1 run since 11 Jul 2026, fewer than the 7 a range needs, so each comparison shown here is a single dated run.

The aarch64 column was measured 11 Jul 2026, 71 days before the newest run here. These are real measurements, not estimates — but they were not taken alongside the others, so read across the tabs as separate runs rather than as one comparison.

What the IPsec figures coverIKEv2 key establishment (IKE_SA_INIT, plus RFC 9370 IKE_INTERMEDIATE). Authentication payloads, which belong to the signature track.

The weighting is the same as TLS on purpose. The composed total uses the same phase weights as the TLS and SSH tracks, because the crypto multiplicity is the same: both peers generate a classical keypair and both derive, and the additional RFC 9370 key exchange contributes one KEM keygen, one encapsulation and one decapsulation. A different weighting was not invented to make this track look distinct.

Two gaps worth naming. MODP (finite-field) groups are not measured. Group 14 (MODP-2048) is still very common in deployed IPsec and is arguably the most representative classical baseline, but measuring it requires the exact RFC 3526 prime -- and a mistranscribed prime would still compute a shared secret and still produce a plausible timing while measuring a group that is not 14. Left unmeasured rather than measured wrongly. MACsec is named in the same CFDIR use case (3.12) and is not measured here. This track's claim on that cell is IKEv2 key establishment only.

An IPsec tunnel rekeys on a timer or byte budget, so the number of key exchanges over its life is far higher than TLS's one per connection. That multiplier is a property of a deployment's configuration rather than of the cryptography, so it belongs to whoever is costing the tunnel. This track measures the cost of one key establishment.
How the handshake figure is builtThe handshake mean is composed, not timed end to end: each phase is measured in its own thousand-iteration loop and the handshake figure is their sum, with the classical keygen and derive counted twice because both parties perform them. That identity holds across every committed suite to within 0.002%, and the smoke test fails the build if it stops holding. It is why the phase bars add to exactly 100% — they are the measurement, not an attribution over it.
Certificates

An ML-DSA-87 certificate chain is 16.8× the size of the one it replaces.

Every TLS connection that isn't resumed carries the server's chain. This is real: chains minted and measured, not key sizes added up.

CertificateSent on the wireAgainst ECDSA-P256
ML-DSA-8714.70 KB16.78×
ML-DSA-6510.88 KB12.42×
ML-DSA-447.89 KB9.01×
RSA-30722.14 KB2.45×
RSA-20481.64 KB1.88×
ECDSA P-256897 Bthe baseline

These are a floor. The certificates behind them carry short names and no Certificate Transparency extensions, where a public certificate carries more — so a real chain is larger, and the post-quantum penalty on a real chain is larger still.

What that costs

Two of them no longer fit in the first round trip.

A server's opening flight has about 14.26 KB before it has to stop and wait for an acknowledgement. Crossing that line doesn't make the handshake bigger — it makes it slower, by a full round trip, on every new connection.

ECDSA P-256
2.20 KB
fits, 12.06 KB spare
ML-DSA-44
11.50 KB
fits, 2.75 KB spare
ML-DSA-65
15.36 KB
over by 1.10 KB
ML-DSA-87
20.47 KB
over by 6.21 KB
RSA-2048
3.14 KB
fits, 11.12 KB spare
RSA-3072
3.77 KB
fits, 10.49 KB spare

Each figure adds three measured parts — a captured ServerHello, the chain above, and the signature over the handshake. The shape of the flight is assumed rather than captured, and it assumes nothing extra: no stapled revocation response, no client certificate, no session ticket. Every one of those pushes the total further over, not back under.

Tokens

A post-quantum access token is 15.7× the one it replaces.

Every signed API call, session cookie and identity assertion carries one of these. Unlike a certificate chain, a token is re-sent on each request — so its size is paid over and over, not once per connection.

SignatureToken on the wireAgainst ECDSA-P256
ML-DSA-876.34 KB15.69×
ML-DSA-654.63 KB11.44×
ML-DSA-443.47 KB8.58×
Falcon-5121.17 KB2.88×
RSA-3072830 B2.00×
RSA-2048660 B1.59×
HTTP cookie value
4.00 KB
2.34 KB over
RFC 6265 section 6.1 - servers SHOULD support at least 4096 bytes per cookie.
nginx large_client_header_buffers (one header)
8.00 KB
fits, 1.66 KB spare
nginx default: 4 buffers of 8k. A single header line must fit within one buffer.
Node.js --max-http-header-size (whole header block)
16.00 KB
fits, 9.66 KB spare
Node.js default since 12.x, applied to the total header block rather than one line.

Every figure above is a configurable default, not a limit of the protocol. Crossing one is a question to ask of your own stack rather than a failure — raise the buffer and the token fits. What it tells you is where a post-quantum token stops being invisible: it is the cookie, not the handshake, that runs out of room first.

Take the data

Every figure traces to a run.

Clone the harness and you should reproduce these within run-to-run variance.

Know the cost of post-quantum migration.

Coldproof