A value that can be checked against its own rules
Most fingerprinting discussion assumes a detector needs something to compare against — a corpus of real devices, a reputation database, a population baseline that says which canvas hashes are common. That assumption is what makes fingerprinting sound expensive, and it is why so much tooling optimises for looking average.
A large class of checks needs none of it. Many web APIs are not free-form key-value stores: the specification, or the shipping implementation, constrains what a legal reading can be. level is a ratio confined to 0–1. rtt is rounded to a fixed grid. deviceMemory is quantised to a power of two. When a value violates one of these rules, the detector does not need to know what a real machine reports — it only needs to know what the API is allowed to report. The evidence is internal to the reading, and self-evident from the spec text.
This matters structurally, not just tactically. A real browser derives these fields from one underlying state: a single power-management subsystem, one network estimator, one heap configuration. A configuration layer that writes each field independently has no shared state to derive them from. It can only guess at each value separately — and separate guesses produce combinations the shared state could never have produced. That is the same argument as coherence over camouflage, sharpened to the point where the disagreeing parties are two fields of the same object.
Mutual exclusion: the BatteryManager case
navigator.getBattery() resolves to a BatteryManager exposing four fields together: level, charging, chargingTime, dischargingTime. Three of them are mutually dependent rather than independent. If charging is true, the device is not discharging, so dischargingTime must be Infinity. If charging is false, it is not charging, so chargingTime must be Infinity. And level is a ratio, so 0 <= level <= 1. Those three assertions are the entire check — Clearcote's open-source audit implements exactly them under the id battery-spec-invariants, and nothing else.
A real implementation cannot fail this, because the fields are projections of one power state. A synthesised one fails as soon as someone sets charging: true next to a finite dischargingTime because the number looked plausible in isolation. No corpus is consulted. The state is simply not one the spec permits.
The nuance is what the check must not flag, and this is where naive invariant checking goes wrong. A desktop with no battery at all reports charging: true, level: 1, chargingTime: 0, dischargingTime: Infinity. Every invariant holds. It is fully coherent and it passes — a check that treats "permanently full and charging" as suspicious is convicting mains-powered hardware for being mains-powered. The audit also deliberately omits the !level guard some consistency tools use to decide the API is unsupported: that guard misreports a genuine 0% battery as an absence, when a real 0 is a reading, not an absence.
Quantisation: the residue, not the magnitude
navigator.connection.rtt, from the Chromium-only NetworkInformation API, does not report a raw measured round-trip time. Chromium deliberately rounds it to 25ms steps as a privacy measure, so every genuine reading is a multiple of 25: 0, 25, 50, 75, 100. The audit's connection-rtt-quantization check is therefore a single expression — rtt % 25 === 0 — and it runs only when rtt is a number.
The consequence is worth stating precisely, because it inverts the usual intuition. The discriminating property is the residue class, not the magnitude. A detector reading rtt=87 learns something structural: 87ms is an entirely plausible round-trip time — arguably more plausible than a suspiciously round 75 — but it is not a number Chromium's quantiser can emit. The value did not come from Chromium's network stack. Effort spent making the number look realistic is effort spent on the axis that is not being measured.
This generalises. Wherever an engine coarsens a value before exposing it, it has replaced a continuous range with a small legal set, and membership in that set is a free lie detector. Realism is irrelevant; legality is everything. The same reasoning drives an adjacent check in the audit — NetworkInformation's mere presence contradicts a UA claiming Firefox or Safari, since only Chromium ships it, which is the surface-area version of the same idea covered in identifying the engine behind the user agent.
deviceMemory: a plausible-but-wrong assumption
navigator.deviceMemory does not report installed RAM. The spec requires the user agent to quantise it to a coarse power-of-two rung before exposing it, which is what makes it a low-entropy hint rather than a hardware readout. The audit's device-memory-bounds check tests membership in {0.25, 0.5, 1, 2, 4, 8, 16, 32}, and treats an absent property as a pass, since undefined is a legitimate state.
The interesting part is what the check refuses to assert. The spec suggests an upper bound of 8, and a check that enforces it is very easy to write and sounds authoritative. But stock Chrome has been observed reporting 16 on high-RAM machines, so bounding at 8 does not catch spoofers — it fails real browsers. The audit's accepted set therefore runs to 32 on purpose, wider than the spec's suggestion, because the quantisation is the durable signal and the ceiling is not.
This is a clean worked example of a failure mode that runs through fingerprinting research generally: a rule that is true of the specification is not automatically true of the implementations. The rule that holds is that no shipping quantiser can produce a 6, a 12, a 3, or a 64. A detector seeing one of those infers the field was written by something other than the engine, and the value itself is the evidence — independent of whether the number is otherwise plausible. A detector enforcing the ceiling instead is mostly generating false positives against ordinary users, which is why the audit scores the rung and not the cap.
Lattices, tolerance, and honest uncertainty
performance.memory.jsHeapSizeLimit is the same idea with a caveat attached. V8 does not scale the limit continuously with installed RAM; it draws it from a small set of discrete values, so real readings cluster on a lattice rather than spreading across a range. The audit compares the reading against four known values — 536870912, 1073741824, 2144337920, 4294705152 — passing if it lands within a 1% relative tolerance of any one. The tolerance is what keeps the check honest: it absorbs minor build-to-build drift instead of demanding an exact byte match.
The value is treated as categorical, not numeric. An arbitrary-but-plausible-looking 3000000000 lands in a gap where V8 never places the limit, revealing that the number was chosen rather than reported. But the set genuinely drifts across V8 versions, which is why jsheap-lattice is a warn and not proof — an off-lattice value is evidence worth surfacing, not a conviction. Saying so plainly is part of the method; a check that overclaims its own certainty is a worse instrument than one that reports a warn.
Contrast hardware-concurrency, which sits in the audit at info severity and is not scored. It only notes whether navigator.hardwareConcurrency is an integer in 1–128, and the bounds are loose on purpose: real machines span that whole range, so there is no invariant to lean on. It cannot tell you a core count is wrong, only that it is outside anything a physical CPU presents. Its research value is as context for other signals — a persona claiming a 32-core workstation alongside 2GB of memory is incoherent as a set, even though each field is individually legal. That is a different and more expensive kind of inference than a modulo.
The through-line
The general lesson is that quantisation and mutual-exclusion rules make lies cheap to detect. These checks are a few lines each, run in the page, need no network call, no corpus, and no population statistics — which means they are available to every detector, not only to the ones with a data advantage. Any surface where the engine coarsens, clamps, or cross-links a value is a surface where a fabricated reading can be convicted on the spec alone.
The practical implication follows directly from the mechanism. These fields fail when they are written independently, so the resolution is to stop writing them independently: derive them from one state, or leave them native and let the engine derive them for you. That is the same conclusion the field-by-field tour in anatomy of a browser fingerprint reaches from the other direction, and the same reason cross-realm coherence is hard for patch-based approaches — one state produces agreeing values everywhere, for free.
To see these specific rows evaluated against a live browser, the checks described here run client-side in the stealth audit, and the broader mechanics are in the detection breakdown. For the shorter conceptual entry point, start with what browser fingerprinting is.