Chrome sends ECT, RTT and Downlink request headers to any server that asks for them with Accept-CH, and builds them from its own network estimate, not from the navigator.connection object a page reads. So when we told the page the round trip was 300 ms, the header on the same browser still said 50. Behind a proxy, the honest estimate already describes the proxy's path, which is why we claim neither.
The switch we had never emitted
Our engine has a switch, --fingerprint-connection-rtt, that sets the round-trip time navigator.connection reports. For months it sat in the list of switches the launcher does not send, beside a reason that ended in the words "and one nobody has read off the wire". It was a guess about what a server sees, written down carefully and never checked.
There was a real case for sending it. Chromium samples network quality per realm, so a window and its own workers can disagree. Measured on one profile, the window read 150/1.75 (round trip and downlink) and its blob and data workers read 100/1.55. Three realms of one browser telling different stories is a seam a page can find with no network access at all.
The case against was a worry. The client hint headers are assembled somewhere else in Chromium, so moving the JavaScript value might leave the header behind and create a contradiction that was ours alone.
What the wire said
We wrote a local origin that sends Accept-CH: rtt, downlink, ect. The hints are what a server receives, and no third-party test page echoes them back, so we had to be the server. One profile, patched in place, a control at both ends, the browser off-screen but headed. Three runs, the same each time:
control, no switches JS 50 header [50]
control again JS 50 header [0, 50]
rtt=300 on the command line
JS 300 header [0, 50]
In both controls the header carries what the page is told. With the switch on, the page hears 300 and the header still says 50.
That priced the trade. Sending the switch removes a seam between realms of our own browser, which costs a detector a worker and a comparison to find. It adds a contradiction between the page and its own request headers, which costs one Accept-CH line. The switch stays off, and the entry in our code now cites this run instead of a worry.
Every profile is drawn from one real computer, not from separate listsIn Mosaic: Machines that exist. GPU, screen, CPU and fonts from one real device. Free holds 3 profiles. Pro is $49 a month for any number.
Mosaic is a desktop app
Email yourself the link and open it on your computer.
How it works: Machines that exist
ECT, RTT and Downlink headers behind a proxy
Chromium builds its estimate from the requests the browser actually makes. A profile behind a proxy sends every request through that proxy, so the round trips it times include the proxy's path, not only the operator's home line. The true value already describes the profile.
Two more reasons were found while building the engine. Chromium rounds the round-trip time to multiples of 25 and downlink to a short ladder of values. Our generator's draw did not, and once produced rtt=110, which no real Chrome emits. And any page can time one fetch and get a real round trip; a claimed figure that a stopwatch contradicts is worse than the truth it replaced.

Three ways the test lied first
Each of these produced a confident, wrong result that looked like a finding about the browser.
- Loopback is too fast to measure. A local origin answers in under a millisecond, so the RTT hint reads
0and downlink sits at its ceiling of10whatever anything else does. The first run said "the header never moves", which was true and was about the server. Stalling every response by 400 ms made the question answerable. - Session restore replayed the previous round. With restore on, each launch reopened the last round's page first. A round's wait was satisfied by the previous round's navigation, and the browser stopped before its own page loaded. Two rounds in three reported
undefined, and the script announced the switch had done nothing. Only the raw request log showed it, with a round's second navigation arriving before its first. - The measurement was more than one value. Chromium revises its estimate while the page loads, so the hinted page is requested more than once, with different hints each time. Reading the first request made the two controls disagree and voided three good runs.
The rule for the control took three attempts to state. "Both controls see the same header value" is wrong, because the real estimate drifts between launches. What has to hold is the relationship: whatever the page is told, the header says the same. With that as the invariant, the test round answered itself.
The sixth time measurement settled it
Our engineering notes record five arguments that reasoning lost to measurement. This was the sixth, and the first where the guess turned out right. That matters too: being right for the reason you stated and being right by luck are different results. We use Google's own build as the control for the same reason, and the same pattern, a flag that moves one place and not another, is what broke the User-Agent on first launch. Values we do change are moved inside the engine, which is the argument of C++, not JavaScript; the rest of a profile is on real fingerprints.






