Hardware concurrency and device memory: the navigator hardware tells
Two of the cheapest numbers a website can read about your machine sit one property access apart. navigator.hardwareConcurrency hands back a logical-core count. navigator.deviceMemory hands back a coarse RAM figure. Neither costs a round trip, neither prompts the user, and both were added so that a page could decide how hard to push the device in front of it. A site that knows you have two cores and half a gigabyte of RAM can ship you the cheap layout. That was the pitch.
The problem is that the two numbers also describe the shape of the box your browser runs in, and a rented box has a different shape than a laptop. A bot fleet running forty headless Chromes on a 96-vCPU server has a hardware profile that no consumer device produces. Spoof one number and forget the other, and the pair contradicts itself. This post is about what those two properties actually return, the clamping and quantization the browsers wrap around them, and why the gap between a stated value and a plausible one is where detectors live.
We will start with hardware concurrency: its range, the per-browser caps, and the reason WebKit and Firefox both refuse to tell you the real core count. Then device memory: the power-of-two rounding in the W3C algorithm, the recommended bounds, and the HTTP client hint that carries the same value server-side. Then the part that matters for detection: the worker-context recheck, the cross-property consistency checks, and the measured statistics on how core counts separate evasive bots from caught ones. The through-line is that both APIs were privacy-coarsened on purpose, and that coarsening is exactly what makes a careless spoof stick out.
What hardwareConcurrency returns, and what it does not
The read-only property returns the number of logical processors available to run threads, a value between 1 and the number of logical processors the user agent is willing to expose. Logical, not physical. A four-core CPU with two-way simultaneous multithreading reports eight. The original use case was sizing a worker pool: spin up one Worker per reported core and you saturate the machine without oversubscribing it. The MDN reference still leads with that loop.
Note the hedge baked into the definition. The browser “may choose to report a lower number of logical cores in order to represent more accurately the number of Workers that can run at once,” so the value is not an absolute measurement of the system. That sentence is doing more work than it looks. It is the spec-level license for every cap and clamp that followed, and it means a detector cannot treat the number as ground truth about silicon. It is ground truth about what the browser decided to say.
The property has been baseline across browsers since March 2022, which makes it one of the more reliable signals to read: present everywhere, same name everywhere, no feature detection needed. It is also exposed inside workers through WorkerNavigator.hardwareConcurrency, and that second exposure point matters later, because a fingerprint that only patches the main-thread navigator leaves the worker copy telling the truth.
Why three browsers cap it and Chrome does not
The fingerprinting concern with a raw core count is uniqueness, and it was understood at the moment the property landed. When WebKit added support in 2014, the engineers negotiated a cap into the first patch. The committed comment is blunt about the motive: enforce a maximum on the number of cores reported “to mitigate fingerprinting for the minority of machines with large numbers of cores.” The arithmetic in the bug thread is the part worth keeping. One contributor noted that an unbounded count leaks something like 21 bits about a unique user, and that even a slight correlation between core count and income turns into money for an ad targeter. Capping at a low power of two collapses the long tail of 16-, 24-, and 64-core machines into the same bucket as ordinary laptops.
WebKit settled on 8 as the ceiling on desktop and 2 on iOS. A 28-core Mac Pro and an 8-core MacBook Air both report 8. Every iPhone reports 2 regardless of how many cores the A-series chip actually has. The cap is a clamp, not a constant, so a 4-core machine still reports 4; the ceiling only bites above it. The effect is that high-end Apple hardware is fingerprint-indistinguishable from mid-range Apple hardware on this axis, which is the entire point.
Firefox arrived at the same idea with a higher ceiling. It clamps hardwareConcurrency at 16, a value that landed for Firefox 97 in early 2022 after a brief detour: the cap was first set lower, then bumped because a chess analysis site got stuck when told the machine had fewer cores than it really did. The reasoning in the bug is the mirror image of WebKit’s. Raising the maximum reported count “will make a minority of users’ fingerprints more unique,” so the engineers wanted a concrete example of a common site made better by reporting 32 instead of 16 before they would lift it. They never got a compelling one, so 16 stuck. Above sixteen logical cores, every Firefox user looks identical here.
Chrome is the outlier. Chromium reports the real logical core count with no cap, which means a Chrome user on a 32-core workstation hands every site a 32, and a Chrome user on a dual-core Chromebook hands over a 2. That decision makes Chrome’s hardwareConcurrency the most informative of the four and the most fingerprintable, and it is the reason the property is worth so much to a detector watching Chrome traffic specifically. The number is not coarsened, so an unusual one is not hidden.
Privacy modes go further than caps. With privacy.resistFingerprinting enabled, Firefox stops reporting the real value at all and pins hardwareConcurrency to a flat 2. The choice of 2 was data-driven: Mozilla’s hardware telemetry showed that around 70 percent of Firefox users had exactly two cores at the time the spoof was designed, so reporting 2 drops an RFP user into the largest available crowd. Tor Browser inherits the same behavior. Brave takes a different route, farbling the value to a random number between 2 and the true count, with the randomization keyed per-site so the same machine reports different counts to different origins. Each of these is a deliberate lie, and each produces a value distribution that a detector can learn.
Device memory and the power-of-two algorithm
navigator.deviceMemory is the younger and blunter of the two. It returns the approximate amount of RAM in gigabytes as a floating-point number drawn from a tiny fixed set: 0.25, 0.5, 1, 2, 4, or 8. The JavaScript API shipped in Chrome 63 in December 2017, with the matching HTTP client hint enabled a couple of versions earlier in Chrome 61. The stated use was the same adaptive-loading story as hardware concurrency: read the RAM bucket, ship a lighter bundle to a 512 MB phone, ship the full experience to an 8 GB laptop.
The coarsening is not a side effect. It is the algorithm. The W3C Device Memory specification, a Working Draft last revised in March 2026 and edited by Barry Pollard of Google and Guohui Deng of Microsoft, defines the value by rounding physical RAM to a single significant bit, the nearest power of two, then clamping the result between an implementation-defined lower and upper bound. The spec is explicit that the purpose is privacy: the reported value is rounded to a single significant bit “as opposed to reporting the exact value,” and the bounds are chosen so the range “should include the majority of device memory values but exclude rare device memory values to mitigate device fingerprinting.” A machine with 6 GB does not report 6. It reports 4 or 8, whichever the rounding lands on.
*The spec brackets the true RAM between two powers of two, picks the closer one, then clamps to the implementation bounds. Any value off this ladder did not come from a real Chrome.*The verbatim steps in the spec are mechanical. Take physical memory in bytes, divide by 1024 to get a working figure, bitwise-shift right until it falls below 1 while counting the shifts to recover the exponent, then set the lower bound to 2 raised to that exponent and the upper bound to 2 raised to one more. Whichever bound is closer to the real figure wins the tie by the rule “if physicalDeviceMemory minus lowerBound is less than or equal to upperBound minus physicalDeviceMemory, then the value is lowerBound.” A final clamp pins anything below the minimum lower bound or above the maximum upper bound. Chrome’s current bounds put the floor at 0.25 and the ceiling at 8, which is why no real Chrome reports 16 or 32 today even though the algorithm could produce them and the spec leaves the bounds open. The MDN page documents a hypothetical browser with bounds of 2 and 32 to make the point that the set of reachable values is implementation-defined, but in practice the only browsers shipping the API are Chromium-based, and they cap at 8.
That last fact is the single most useful thing about device memory for detection. Firefox and Safari do not implement the property at all. Reading navigator.deviceMemory in either returns undefined. So the property is doing double duty: its value is a coarse RAM bucket, and its mere presence is a claim that the browser is Chromium. A user agent string that says Firefox while deviceMemory returns a number is two browsers wearing one coat.
The client hint twin
The same value travels server-side as an HTTP client hint, which means a backend can read it before a single line of JavaScript runs. A server opts in by sending an Accept-CH response header listing the hint, and on subsequent same-origin requests over HTTPS the browser attaches Sec-CH-Device-Memory (the older spelling was the bare Device-Memory) carrying the same power-of-two figure. A server that wants the RAM bucket sends Accept-CH: Sec-CH-Device-Memory and gets back Sec-CH-Device-Memory: 8 on the next hop. The header reports the identical quantized value as the JavaScript property, drawn from the same 0.25-to-8 set.
This dual surface is a gift to anyone checking for consistency. A browser that reports one RAM bucket in JavaScript and a different one in the header is inconsistent in a way no genuine Chrome produces, because both come from the same internal computation. An HTTP client that forges the header but cannot run the JavaScript to back it up, or a headless browser whose header was rewritten by a proxy while the in-page property was left alone, splits the two values apart. The header is also where a non-browser scraper most often gives itself away by sending a memory hint it was never asked for, since a well-behaved client only sends Sec-CH-Device-Memory after the server advertised Accept-CH. An unsolicited hint is a script copying a captured request without modeling the opt-in handshake.
The worker recheck
Both properties are exposed inside workers, and that is not redundancy. It is a second reading point that a naive spoof forgets. When a fingerprinting script patches navigator.hardwareConcurrency on the main thread by redefining the property, the override lives on the main-thread navigator object. Spin up a Worker or a SharedWorker, read self.navigator.hardwareConcurrency from inside it, and you get the engine’s real answer, because the worker has its own WorkerNavigator that the page-level patch never touched. The same trick works for deviceMemory.
This is the standard shape of a stealth-patch failure. A bot framework hooks the property getter on window.navigator to return a believable 8, the detector reads it once on the main thread and sees 8, then reads it again from a freshly spawned worker and sees 24, the real vCPU count of the rented host. Two values, same machine, same instant. There is no legitimate browser configuration in which the main thread and a worker disagree about the core count, so the mismatch is a clean tell. The general technique of cross-checking the worker realm against the document realm is its own subject, covered in iframe and worker context fingerprinting; the hardware properties are simply two of the easiest values to catch out this way, because they are read-only primitives that a spoof has to override in every realm or none.
The same logic applies across an iframe boundary. A cross-origin iframe gets its own navigator, and a patch installed in the top document does not automatically propagate into a child frame’s realm unless the automation tooling injects into every context. Detectors lean on this because injecting consistently into every realm, including ones created after the page loads, is harder than it sounds, and the failures are deterministic rather than probabilistic.
What an impossible value looks like
Quantization is the detector’s friend because it shrinks the space of legal answers to a short list. Hardware concurrency is always a positive integer. Device memory is always one of six floats. Anything off those ladders is, by construction, not something a stock browser produced.
Consider device memory first, since its legal set is tiny. A real Chrome cannot report 3, or 6, or 12. Those are not powers of two, and even the powers of two above 8 are unreachable because of the clamp. So a request claiming deviceMemory: 16 or deviceMemory: 6 is either a browser build that nobody ships or a spoof that did not model the rounding. A security write-up from Castle in 2025 reported observing more than 16,000 Chrome-labeled events in a single week of traffic that carried a deviceMemory greater than 8, a value the real Chrome algorithm cannot emit, with one cluster of roughly 3,500 of those events tied to a single fake-account operation using disposable email domains. The value did not need to be improbable to be useful. It needed to be impossible, and impossibility is a property the quantization scheme hands you for free.
Hardware concurrency is harder to make outright impossible, since any positive integer is a syntactically valid core count, but it is easy to make implausible. A value of 1 is rare on modern consumer hardware and common on the smallest cloud instances. Very high values, 64 or 128, do not match the laptop or phone that the rest of the fingerprint claims to be. And odd numbers above a small threshold are suspect because consumer core counts cluster on even numbers and powers of two; a reported 7 or 13 suggests a value that was assigned by a config file rather than measured from silicon. None of these is conclusive alone. Each shifts a probability.
The sharper signal is the cross-property check. Hardware concurrency and device memory are correlated in the real world. A two-core phone does not have 8 GB of reported RAM, and a 32-core workstation does not have 0.25 GB. The joint distribution is far narrower than the product of the two marginals, so a pair that is individually plausible can be jointly absurd. Two cores with 8 GB, or 32 cores with half a gig, describes no shipping device. The same logic extends to the other properties the user agent advertises: a platform of iPhone paired with a deviceMemory value at all is already wrong, because iOS Safari does not implement the property. The detail of which navigator properties a detector reads together, and how they constrain each other, is the subject of the navigator object as a fingerprint; the hardware pair is one of the tightest joint constraints in that set.
The numbers detectors actually see
The strongest public evidence that core count separates real users from bots comes from the FP-Inconsistent study out of UC Davis, published in 2024, which measured fingerprint inconsistencies in evasive bot traffic against DataDome. The headline figure: 84.7 percent of requests from bot services with a high evasion rate against DataDome had fewer than 8 cores, against only 38.16 percent of requests from bot services that DataDome caught. Read those two numbers together. Bots that slip past detection cluster hard at low core counts; bots that get caught are spread out, with a far larger share claiming 8 or more. The implication is that successfully evasive bots are deliberately reporting small, common core counts to blend into the crowd of ordinary laptops, while the caught ones leak the larger core counts of the servers they actually run on.
That is the whole dynamic in one statistic. A 96-vCPU server running headless Chrome, if it reports the truth, hands the detector a number that no consumer laptop produces, so the careful operator clamps it down to a plausible 4 or 8. The clamp is itself a tell once the detector knows the population, because the distribution of a real consumer fleet has a long but smooth tail, and a bot fleet that has been hand-tuned to report exactly 4 or exactly 8 piles up at those values in a way that real hardware does not. The defender is not looking for one impossible value. The defender is looking at the shape of the whole distribution and noticing where it bunches.
The same study frames the general method as spatial versus temporal inconsistency. Spatial inconsistency is two attributes from a single request that cannot both be true, the joint-absurdity case above. Temporal inconsistency is the same device reporting incompatible values across requests over time, a core count that changes between page loads when no real machine’s core count changes. A residential proxy fleet that rotates which physical box serves a given session, while reusing one fingerprint profile, produces exactly this: the cookie says one device, the hardware numbers drift. Hardware concurrency and device memory are good temporal anchors precisely because they should never move for a real user. The day they move, the session is lying about being one machine.
This single-vector view connects to the wider picture in two directions. Upstream, the core count and RAM bucket feed the same scoring pipelines that weigh dozens of other signals; how a vendor like DataDome assembles those into a verdict is covered in DataDome’s detection model. Downstream, the deeper problem for an evader is that fixing the hardware numbers in isolation does not help, because the runtime exposes a hundred other properties that have to agree, the subject of how anti-bot systems fingerprint the JavaScript runtime. The hardware pair is two coordinates in a space with far more dimensions, and consistency across all of them is the actual bar.
Closing
The two numbers were never meant to be secrets. They were meant to be hints, coarse enough that a page could size a worker pool or pick a bundle without learning the user’s exact hardware. The privacy engineers who added the caps and the power-of-two rounding were trying to spend as little entropy as possible while keeping the hint useful. What they built, almost as a side effect, is a pair of values with a very short list of legal answers and a very tight correlation with each other and with the rest of the navigator object.
That short list is what makes a careless lie visible. There is no honest way for a real Chrome to report 6 GB of memory, no honest way for an iOS Safari to report any memory at all, and no honest way for a main thread and a worker on the same machine to disagree about the core count. A bot that wants to pass has to get every one of those right at the same time, in every realm, and keep them stable across every request in a session. Getting the visible number right while the worker copy, the client hint, or the joint distribution stays wrong is the ordinary failure, and it is the one the measurements keep finding. The cap that was meant to protect a 32-core user’s privacy is the same cap that makes a 32-core server pretending to be a laptop stand out the moment it forgets to clamp.
Sources & further reading
- W3C (2026), Device Memory 1, Working Draft 30 March 2026 — the normative rounding algorithm, the implementation-defined bounds, and the fingerprinting-mitigation rationale.
- MDN (2024), Navigator: hardwareConcurrency property — definition, range, the “may report a lower number” caveat, and baseline availability since March 2022.
- MDN (2024), Navigator: deviceMemory property — returned values, power-of-two coarsening, secure-context requirement, and worker availability.
- Walton, P., Chrome for Developers (2017), The Device Memory API — the original Chrome 63 launch, the Device-Memory client hint, and the adaptive-loading use case.
- WebKit (2014), Bug 132588: support for navigator.hardwareConcurrency — the original cap negotiation and the “mitigate fingerprinting” comment in the landed patch.
- Mozilla (2022), Bug 1728741: navigator.hardwareConcurrency capped at 16 — the Firefox 97 ceiling, the back-out over a chess site, and the reasoning on per-user uniqueness.
- Mozilla (2017), Bug 1360039: Spoof navigator.hardwareConcurrency = 2 under resistFingerprinting — the RFP/Tor spoof value and the 70-percent-of-users data behind choosing 2.
- Brave (2022), Issue 5268: Fingerprinting 2.0 hardwareConcurrency — farbling the value to a per-site random number between 2 and the true count.
- Venugopalan, H. et al., UC Davis (2024), FP-Inconsistent: Measurement and Analysis of Fingerprint Inconsistencies in Evasive Bot Traffic — the 84.7-percent core-count statistic and the spatial-versus-temporal inconsistency method.
- Vastel, A., Castle (2025), Deep dive: how navigator.deviceMemory can be used for fingerprinting and bot detection — presence checks, impossible-value observations, and the 16,000-event traffic sample.
- MDN (2024), Sec-CH-Device-Memory header — the client-hint twin of the JS property and the Accept-CH opt-in handshake.
Further reading
Device fingerprinting in anti-bot stacks: FingerprintJS, the entropy budget, and stability
How device fingerprinting works in anti-bot stacks, traced through FingerprintJS open-source and Pro: the signal set, the entropy budget that makes a visitor ID unique, why client-side hashes drift, and how it differs from bot detection.
·23 min readCanvas fingerprinting: how a single toDataURL call identifies a device
Traces how rendering text and shapes to an HTML5 canvas and hashing the toDataURL output yields a stable per-device value, the GPU, driver and font causes behind the variation, the 2012 origin, and how much entropy it really carries.
·22 min readWebGL fingerprinting: the renderer string, precision, and shader quirks
A primary-source reference on WebGL fingerprinting: the UNMASKED_RENDERER and UNMASKED_VENDOR strings, supported extensions, shader precision formats, rendered-image hashing, and the browser mitigations that bucket or hide them.
·24 min read