Skip to content
Detection teardowns

Latency as a location check: what a WebSocket echo says about your proxy

The server's TCP handshake measures the distance to the last proxy hop. A timestamp echoed over a WebSocket measures the distance to the code that is really running.

Pim
Pim· Clearcote Research
9 min read

Key takeaways

  • A WebSocket that echoes performance.now() measures the round trip to the software that runs the page; the TCP handshake on the same connection only measures the round trip to whatever terminated TCP, which behind a proxy is the proxy exit.
  • Measured from Amsterdam with Chrome 156: about 12 ms direct, about 50 ms through a Dutch residential exit and 400 to 470 ms through a US exit, while the server saw a home IP each time.
  • Published systems flag gaps above about 50 ms with false-positive rates around 1 % or lower; a residential exit in the user's own country (+38 ms in our test) can stay under that line.

Two round trips on one connection

Every TCP connection starts with a handshake: the client sends SYN, the server answers SYN-ACK, the client sends ACK. The server can time the gap between its SYN-ACK and the ACK, which gives it the round-trip time to whatever opened the connection. If a browser connects directly, that is the browser's machine. If the browser goes through an HTTP CONNECT proxy, a SOCKS proxy or a residential proxy network, the proxy opens a new TCP connection to the server, and the handshake only measures the last hop.

An application-level echo is different. If the page sends a timestamp and the server echoes it back, the reply has to travel all the way to the code that sent it and back. Through a proxy that means browser to proxy, proxy to exit, exit to server, and the same path back. The application round trip includes every hop; the TCP round trip includes only the last one.

On a direct connection the two numbers are close. Behind a proxy that terminates TCP there is a gap, and the gap is roughly the length of the hidden path.

The probe a challenge widget added

A widely deployed challenge widget added this measurement in builds released from 27 October 2026. Inside its iframe it opens a WebSocket to its own challenge host and sends String(performance.now()) as soon as the socket opens. For each echo it computes performance.now() - Number(echo), records it, and sends a fresh reading, until six echoes have come back. A 500 ms timeout ends the exchange.

The record holds the status (complete, incomplete, timed out, failed before or after opening, closed), how many messages went out and came back, the time from creating the socket to its open event, the six round-trip times at full float precision, and the total duration. One captured example: the socket opened after 101.5 ms and the six echoes took between 9.4 and 11.9 ms.

The same build added a second probe that fetches a list of the vendor's own hosts from a Web Worker with cache: "no-store", no credentials, no referrer and redirects treated as errors, and reports the time and outcome of each. Without a cache, each fetch includes a fresh connection, so the results form a small latency map from wherever the traffic really exits.

What the server does with these values cannot be observed from the outside. The pattern matches the published technique below.

What published work measured

  • CalcuLatency (USENIX Security 2024) compares a WebSocket round trip measured from the page with the TCP handshake round trip and with network-layer probes. With a 50 ms threshold on the difference it reported a 0.95 % false-positive rate and a 2.9 % false-negative rate. Two thirds of the VPNs it missed were within about 1,050 km of the user. Browser overhead was small: 10,000 loopback echoes had a median under 1.4 ms.
  • BADPASS (ISPEC 2022) compared the TLS handshake round trip with the TCP round trip on the server side, over 92.7 million connections through four residential proxy providers. 97 % of direct connections differed by 20 ms or less; at a 50 ms threshold the false-positive rate was 0.04 % and the false-negative rate 1.93 %.
  • Cross-layer RTTs (NDSS 2025) applied the same principle from a network observer's position and found detectable flows for about 80 % of popular domains visited through an obfuscated proxy.

Network-layer VPNs are the exception. A WireGuard or OpenVPN tunnel carries the client's own TCP handshake, so TCP and WebSocket round trips both include the full path and show no gap. CalcuLatency adds ICMP pings and in-connection traceroutes for that case. Proxies that tunnel at the application layer, which includes most scraping proxies and residential proxy networks, are the case this probe is built for.

What we measured

We ran the same pattern, String(performance.now()) echoed six to twenty times, against a public WebSocket echo server from a Linux machine in Amsterdam with Chrome 156:

  • Direct: echoes of 11.7 to 17.7 ms, medians around 12.5 ms; the socket opened in 47 to 62 ms. A bare Node.js client got the same, so the browser adds almost nothing.
  • Through a Dutch residential exit (a home connection about 6 ms from the server): echoes of 48 to 64 ms, about 38 ms more than direct; the socket opened in about 272 ms.
  • Through a US exit: echoes of 400 to 470 ms; the socket opened after 1.6 to 1.8 s.

With a 500 ms budget that starts before the socket is created, the direct run finishes all six echoes in about 120 ms; the Dutch exit would time out after about four echoes, and the US exit before the socket even opens. The status string alone already sorts sessions by distance.

The Dutch result also shows the limit of the technique. A residential exit in the same country added 38 ms, under the 50 ms threshold both papers use. Latency catches distance, not proxies as such.

What follows for automation

No browser setting changes a round trip. A session behind a distant proxy will show the distance in every echo, whatever its IP, timezone and language say, and the gap between the application round trip and the TCP round trip is measured on the server, out of reach of the page. The practical consequences are about network design: an exit close to where the browser actually runs, a short chain of hops, and timezone and locale that fit the exit, as described in timezone and IP mismatch.

Clock precision matters for the measurement itself: the echo values are differences of two coarsened performance.now() readings, so their granularity also shows which browser's clock produced them. See performance.now() precision.

The through-line

Like the proof of work, the echo measures something physical. A fingerprint can claim a location; it cannot shorten the path a packet takes. The whole probe list is in inside a challenge widget, and the WebRTC leak test covers the other network path that has to agree with the proxy.

#bot-detection#proxies#reference

Clearcote puts this into practice

An open-source Chromium with fingerprint control compiled into the engine. A drop-in for Playwright & Puppeteer.

  • 228engine patches
  • 34,679lines added
  • 336Chromium files changed
  • 354checks per build

Engine numbers are for the Pro build; the free open build is a smaller, public patch set.

Free for one browser with GitHub. No card.