Field notes
Deep-dives on how browser fingerprinting and bot detection actually work, one dimension at a time, grounded in reverse-engineering real detectors and testing real anti-detect browsers. New posts land about once a week.
Beyond width: the other eleven numbers TextMetrics gives you
Metric-compatible substitutes match advance widths by design. They do not match the font bounding box, the ink extents or the baselines, and measureText returns all of those from the same call.
The layer JavaScript never sees: TLS, JA3/JA4 and HTTP/2 fingerprints
Before a line of your JS runs, the TLS handshake and HTTP/2 frames have already fingerprinted the client. It's the wall a JS-layer spoof can't touch and the place where a real Chrome build pays off.
Three majors behind: version lag is a fingerprint of its own
Chrome ships a major every four weeks and updates itself without asking. A browser several versions back is not a user who forgot, it is a pinned build, and that narrows the anonymity set before anything else is read.
DOMRect and subpixel geometry: the fingerprint DataDome loves and the dpr trap that breaks spoofers
getClientRects returns floating-point layout geometry that varies by device and CreepJS hides a hardcoded hash that only fires when devicePixelRatio equals 1.
Which signal actually blocked you: running a controlled bisect
A rejection tells you nothing about which of forty signals caused it. One variable at a time, a control arm on both ends, and the discipline to accept that four fixes in a row changed nothing.
Workers and OffscreenCanvas: fingerprinting in the thread you forgot to patch
Detectors re-read identity inside a Web Worker and re-run canvas in OffscreenCanvas, a favorite way to catch spoofs that only hooked the main thread.
The plugin face Android never had: five entries that kill a mobile persona
Chrome on Android is built without the PDF viewer, so navigator.plugins is empty, mimeTypes is empty and pdfViewerEnabled is false. A desktop build wearing a phone persona reports 5, 2 and true.
The plugin face: modern Chrome's five PDF entries and the version it betrays
navigator.plugins looks vestigial, but its exact shape is pinned to the Chrome version, and a stale plugin face is a clean contradiction detectors love.
Static says phone, events say mouse: the device-class contradiction
A mobile persona sets maxTouchPoints and pointer:coarse and passes every static check. Then the page listens, and every event that arrives is a mouse event from a machine with a mouse.
[native code]: why JS-override stealth loses to engine-level spoofing
Every getter you redefine in JavaScript leaves a residue a lie-detector can read. The split between JS-patched and engine-level browsers shows up in one toString() call.
Mobile texture formats on a desktop GPU: the capability table under the string
Rewriting UNMASKED_RENDERER takes one line. The extension list, the WebGPU feature set and the shader precision table underneath it are produced by the driver, and they name the chip that is really there.
Timezone vs IP: the cross-layer check that catches proxies
A US exit IP paired with an Asia/Singapore browser clock is one of the cheapest, most reliable bot tells there is, and two well-funded anti-detect browsers shipped exactly that.
A window cannot be bigger than its screen: the geometry invariants
Screen spoofing usually rewrites screen.width and stops there. The window, its position, the menu bar inset and the multi-monitor flag all keep describing the real display, and they have to agree with each other.
Audio fingerprinting: the oscillator, the compressor and why silence must stay silent
The Web Audio fingerprint is stable to many decimal places and the fastest way to fail it is to add noise where a real browser outputs pure zeros.
The interface census: an Android persona is a different build, not a different User-Agent
Chromium decides which Web APIs exist per platform, at build and configuration time. A desktop binary running a phone persona still answers typeof navigator.hid with 'object', and no identity layer moves that answer.
Named but not measurable: the font check every cross-OS persona fails
An allow-list can put Segoe UI in the enumerable font set on a Mac. It cannot give it metrics. Every family the host has no file for measures the same width, and that one number is enough to call the persona a lie.
WebGPU, the new GPU oracle: adapter.info and the cross-interface triad
WebGPU exposes a second, independent view of your GPU. If it disagrees with WebGL and the UA platform, the spoof falls apart. Most anti-detect browsers shipped that mismatch for years.
WebGL fingerprinting: it's the whole parameter set, not just the renderer string
Spoofing UNMASKED_RENDERER is the easy 5%. Detectors read dozens of getParameter values, shader precision and the actual rendered pixels and 'self-consistent' is not the same as 'believable'.
Automation tells are orthogonal: CDP, Runtime.enable and the isolated world
A flawless fingerprint won't hide the driver. navigator.webdriver, CDP execution-context leaks and headless tokens are a separate axis every serious detector checks.
Font fingerprinting without a font API: the offsetWidth measurement method
The dominant font-enumeration technique never calls a font API at all. It measures span widths. That is why patching document.fonts alone gets you caught, and why CJK fonts leak your real OS.
Inside the CreepJS lie detector: four checks deterministic noise can't beat
CreepJS doesn't only collect signals. It proves them self-consistent. Here are the four invariant classes that catch spoofers even when their noise is perfectly stable.
Canvas fingerprinting and the deterministic-noise rule
Anti-detect browsers get caught on canvas because their noise changes between two identical reads. The size of the noise is almost beside the point. Determinism is the whole game.
Two layers, one browser: why consistency beats any single fingerprint
A perfect JavaScript fingerprint still fails if it disagrees with the network layer. Modern detection is a cross-layer consistency problem, not a collection problem.