I wonder if it could be done better using WebSockets: setup the connection, start the stopwatch, send a byte, wait to receive a byte, stop the stopwatch, and close the connection.
Modern browsers / webservers will keep TCP connections alive for at least a few minutes. I ran a tcpdump and confirmed there's only one network round trip involved in the critical path (after the first request, anyhow), with a transfer of a few hundred bytes in each direction (HTTP overhead, but nowhere near big enough to incur processing delays on the same scale as propagation delay).
(The actual packets sent: HTTP GET from my end, HTTP response from the server, ACK from my end.)
The latency is still 20-40ms higher than ping times, although it's not clear to me whether that's due to disk seek latency, server load, or something else.
This is super helpful context, thank you! I was wondering why the RTTs appeared to converge only after a couple retries, and why the first measurement was so wildly different when pulling up the page after some time away.