Notes3 min read
Adding noise is the tell, not the disguise
We shipped fingerprint noise twice, on two unrelated axes, and had to take it back out both times. Here is what the numbers said.
The standard advice for hiding a browser fingerprint is to perturb it: read the canvas, add a tiny random offset to every pixel, and the value stops matching across profiles. It works. It also produces the single most legible signal we have ever measured.
We shipped that noise. Then we shipped a second kind, on a completely unrelated axis. Both came back the same way.
What we did to the canvas
The change was small: every canvas readback got a deterministic ±2 offset, seeded per profile. It did exactly what it promised — full unlinkability across a fleet. Two profiles on the same machine no longer shared a canvas hash.
Then we ran it past a commercial detector, and the verdict came back as
manipulation.
That is worth sitting with. The detector did not say "unusual hardware" or "unknown device". It said the value had been tampered with. A flat delta applied to every pixel of a rendered image is not camouflage; it is a signature. Real hardware produces variation that follows the shape of the rendering pipeline — edges, anti-aliasing, subpixel geometry. A uniform offset follows nothing.
We disabled it on every readback surface. Canvas, WebGL, WebGPU, measureText
and audio all report the honest device value now.
The cost of that decision
Being honest about the canvas means a fleet on one Linux host shares one canvas fingerprint. That is a real trade and we make it deliberately: a shared honest value is ordinary — millions of machines report the same thing — while a manipulated value is unique in the way that gets you looked at.
Per-profile canvas diversity would require physically distinct GPUs per profile. We do not pretend otherwise, and we do not sell it.
Then it happened again, on the pointer
Months later, on an axis with nothing in common with graphics: we added physiological tremor to the pointer path. Human hands shake; a synthetic straight line does not. Adding a small oscillation seemed obviously right.
We measured the result against a 120-subject human cohort. Our tremor produced four times the direction-reversal rate of real people.
The reason turns out to be simple, and it is the same reason as the canvas. Real physiological tremor is roughly 8–12 Hz at an amplitude far below one pixel — it never survives the browser's own sampling and never reaches the DOM at all. What reaches a page is the shape of a corrective movement: overshoot, a pause, a small return. That is a distribution, not a jitter.
So we stopped perturbing and started matching. The pointer paths come from recorded people, and the statistic that gave us away is now inside the human range rather than four times outside it.
The rule we ended up with
Added noise is a tell, not a disguise — on every axis.
Two independent findings, two years apart in the codebase, on axes that share no code. Both said the same thing: match the real distribution instead of perturbing a fake one.
It is also the reason we publish the things we got wrong. Every one of these shipped first.
Everything above was measured on a live site, with the same tools you get.
Start free