Back to Knowledge Hub
Fingerprint

How navigator.hardwareConcurrency Exposes Your Real CPU Core Count

Use navigator.hardwareConcurrency and Worker benchmarks to see how browsers report CPU cores and what that signal can reveal.

BotBrowser Team

Documentation

Prefer the maintained product doc?

This article has a matching page in the docs center. Use the docs for the canonical setup flow, current flags, and long-term reference.

navigator.hardwareConcurrency is a single number, yet it is one of the hardware hints that any script can read without a permission prompt. It reports how many logical processors the browser may use, and it is available in the main thread and in every worker type. A site that wants to judge whether a device description is coherent can compare that number with the other hardware values it sees and with the way parallel work behaves in practice. The sections below cover what the value means, why it differs between machines, how much it adds to identification, and what a consistent, profile-backed core count has to satisfy. BotBrowser can report one profile-backed core count consistently across the contexts of a controlled test; it cannot change how many cores the host machine has or how a site uses the value.

Diagram of one profile core count reported to the main thread, Worker, SharedWorker, and ServiceWorker, with parallel scaling aligned on Linux and Windows

Watch the video

This English-language video tests four claims about the reported core count in a controlled lab page: the Console readback, the worker contexts, a timed scaling run on one Ubuntu host, and what the post says a profile changes. It shows two hosts, not a population.

TL;DR

  • navigator.hardwareConcurrency reports logical processors, not guaranteed throughput.
  • Virtual machines, containers, browser policy, and hybrid CPUs can change the reported value.
  • Compare the value in the main thread and workers with the profile's platform and memory hints.
  • BotBrowser keeps a profile-backed value consistent; Linux and Windows can also align Worker CPU affinity.

Contents

What navigator.hardwareConcurrency reports

navigator.hardwareConcurrency can reflect a guest limited to 2 vCPU, rather than its host processor count.

The property returns an integer that counts the logical processors available to run threads. Logical processors are physical cores multiplied by hardware threads per core, so a four-core processor with simultaneous multithreading reports 8. MDN describes the property as the number of logical processors available to the browser, and the WHATWG HTML specification adds that a user agent should lean toward exposing the real number and use a lower value only where a limit exists or where reducing fingerprinting is the goal.

// Main thread
console.log(navigator.hardwareConcurrency);
// Inside a dedicated worker, shared worker, or service worker
console.log(self.navigator.hardwareConcurrency);

The number is a planning hint, not a measurement of speed. It does not say how fast a core is, whether the cores are efficiency or performance cores, how busy the machine is at this moment, or how many threads the operating system will actually schedule for the page. Two machines that both report 8 can behave very differently under the same parallel workload.

Applications read the value for sensible reasons, such as choosing the size of a worker pool, so browsers cannot simply remove it without breaking working code. It also needs no permission and is exposed to every worker type. That combination is why the number stays visible and why consistency across contexts, rather than removal, is the practical goal.

For application code the safe pattern is to treat the value as an upper bound for a worker pool, leave capacity for the main thread, and measure real throughput before settling on a size. A pool equal to the reported count can compete with rendering and input handling, and a pool larger than the count rarely helps CPU-bound work because the extra threads wait for a processor. The same number should never be used as an authorization decision or as a reason to collect a more detailed hardware profile.

Why the number differs between machines

On Linux, a container can give the browser a cpuset.cpus allowance such as 2 vCPU instead of the host's 16-core total.

Several factors decide what a browser reports.

  • Processor model. The core and thread count come from the CPU. Many mainstream desktop processors report 8 or 12 or 16 logical processors, while older and low-end machines may report 2 or 4. Apple silicon and other hybrid designs mix efficiency and performance cores, and the total depends on the chip.
  • Virtual machines. A virtual machine reports the virtual CPUs assigned by the hypervisor. A host with 16 cores that gives a guest 2 virtual CPUs shows 2 inside the guest, regardless of the hardware underneath.
  • Containers and operating system limits. Container runtimes and scheduler settings can restrict how many processors a process is allowed to use. Whether the browser sees such a restriction depends on how the limit is applied.
  • Browser policy. The specification lets a user agent report a lower value where it sets limits or wants to reduce fingerprinting, and some browsers have experimented with capping or bucketing the number. Chromium currently reports the count the operating system exposes.

This variation matters to developers before it matters to anyone else. The value read inside a CI container or a cloud instance often looks very different from the same browser on a laptop, so a test that assumes a particular count can pass in one place and fail in another. Read the number at run time and treat it as an input, never as a constant.

Typical values follow the hardware market. On desktops, 4 and 8 are the most common, with 6, 12, and 16 appearing on specific processor families. Anything above 16 is mostly found on workstations and servers. Phones commonly report 4, 6, or 8, depending on the chip design. Odd counts such as 7 or 13 are rare among shipping processors, which becomes important when someone considers picking a number by hand.

When a workload moves between environments, compare the reported count rather than the advertised instance size. A cloud instance sold with a number of virtual CPUs can report a different value inside a container, and a laptop with efficiency cores can report more logical processors than a casual reading of its specification suggests. Logging the observed value next to each benchmark result removes a common source of confusion when two runs differ. In a virtual machine the reported value also matters for the scaling check described below, because a guest with 2 virtual CPUs genuinely cannot run more than two threads at once. A consistent environment is one where the reported count and the available processors agree.

How the signal adds to identification

Fingerprinting studies measure how much each attribute narrows down a population. AmIUnique data puts navigator.hardwareConcurrency at roughly 4.5 bits of entropy, which is the equivalent of separating about 23 equally likely groups. The real distribution is far from equal: a few common values cover most browsers, and the rest are rare, so the practical split is coarser than the headline number suggests.

On its own the value is a contributing signal, not a unique identifier. Entropy accumulates when attributes are independent. Each additional bit roughly halves the set of matching browsers, so a core count combined with a screen size, a time zone, and a font list narrows a population quickly. Correlated attributes add less, because a user agent that names a specific phone already implies a short list of plausible core counts.

A rare value narrows further. A report of 24, 32, or 64 suggests a workstation or server, which is uncommon among everyday browsing populations. The number says nothing about a person, but it can make a session easier to single out among others that report 4 or 8.

Mobile devices add another layer. Specific chip families pair a particular count with other properties such as the platform string and the screen. A phone that reports 8 together with a specific user agent pattern narrows the device to a short list of models, which is more information than either value gives alone. The same logic applies to desktops: a count that is common by itself becomes more distinctive when it is paired with an unusual memory value or platform.

Privacy-oriented browsers treat this trade-off differently. Capping or bucketing the number reduces the information it carries, at the cost of giving applications a less accurate hint for sizing worker pools. Both choices are legitimate. The useful question for a developer is how much the application depends on an accurate count, and for a privacy reviewer it is how many other hardware hints are exposed alongside it.

These numbers are reasons for caution, not a verdict. A browser that reports a common count is not protected from other signals, and a browser that reports an unusual count is not automatically flagged. The count is one input among many, and its weight depends on what else the page can see.

How a core count gets cross-checked

The comparison starts with navigator.hardwareConcurrency and can include a value returned by a Worker created from the same page.

Beyond the number itself, a site can ask whether the number fits. Three kinds of consistency matter when a core count is reported.

  1. Plausibility against other values. A core count should make sense together with the platform, the user agent, and the device memory hint. Two cores with 8 GB of memory describes an ordinary small laptop or virtual machine. A 64-core count next to a phone user agent describes hardware that does not exist.
  2. Agreement across contexts. The main thread, dedicated workers, shared workers, and service workers should report one value. A single page script can read all of them, so a gap between contexts is easy to notice.
  3. Agreement with parallel behavior. A script could run the same job on a growing number of Workers and watch where completion time stops improving. A machine with 4 logical processors flattens near four Workers, while one with 16 keeps improving longer.

The pairs worth checking are concrete. A Windows desktop user agent with 8 or 16 logical processors and 8 GB of reported memory is common. A Linux x86_64 platform with 4 cores points to ordinary desktop or server hardware, while an ARM platform string with 8 cores suggests a phone or a specific ARM server. None of these pairs proves anything alone; each one only has to avoid contradicting the rest of the description.

Context agreement deserves attention because workers are created from the page and share its origin, yet each has its own global scope. A dedicated worker, a shared worker that several tabs use, and a service worker that outlives a page all read the property independently, so any change made for the page has to be applied in each of them and verified on its own.

The third check is a possibility more than a routine measurement. Browsers reduce timer precision, and shared-memory timing tools such as SharedArrayBuffer require a cross-origin isolated page with the COOP and COEP headers set. Those requirements limit how precisely a page can measure scaling, so treat the check as one more reason to keep the reported number and the real behavior aligned, not as a certain outcome.

How any given site weighs these checks cannot be observed from outside. They are consistency goals, not predictions of what a particular site will conclude.

Why manual overrides create contradictions

Overwriting Navigator.prototype.hardwareConcurrency in a page does not update WorkerNavigator.hardwareConcurrency in a separate worker global.

The common way to change the value is to overwrite the property from an extension or an injected script. That changes the number the main thread returns, and it leaves three gaps.

  • Scaling mismatch. If the property says 4 while the machine has 16 cores, parallel work finishes sooner than a 4-core machine would allow. The claim and the behavior disagree whenever someone measures.
  • Context gaps. An override applied only to the page's navigator object does not reach worker scopes unless it is injected there as well, so a worker can still return the real value.
  • Partial device description. A new number that is not coordinated with the user agent, the platform, and the memory hint can contradict them.

Random values make the third gap worse. A count of 7 or 13 rarely matches shipping hardware. Even a fixed common value such as 8 contradicts a user agent that names a phone whose chip has 4 cores. A single property changed in isolation is only as trustworthy as the weakest of the other values around it.

The context gap is the easiest of the three to find. A test page can create a dedicated worker from a Blob URL, post the value back, and compare it with the main thread in a few lines. A shared worker and a service worker take a little more setup, but the comparison is the same, and any difference is a defect in the override rather than a property of the hardware.

The requirement is therefore consistency: one value, reported in every context, that fits the rest of the device description and agrees with the way parallel work scales. Keeping that consistency by hand across all of those surfaces is the part that becomes risky.

Checking a profile-backed core count

The first check records navigator.hardwareConcurrency in the main thread and in each Worker.

A profile-backed setup takes the number from a complete device profile instead of from a hand-set override. There is no separate core-count flag, because the value comes from the profile. Four checks show whether the result holds together.

  • Value. Read the property and confirm it equals the profile's core count rather than the host's.
  • Contexts. Read it inside a dedicated worker, a shared worker, and a service worker, and confirm each matches the main thread.
  • Plausibility. Confirm the count is realistic for the profile's platform, user agent, and device memory.
  • Stability. Read it across several sessions with the same profile and confirm it never changes.
const source = 'postMessage(self.navigator.hardwareConcurrency)';
const blob = new Blob([source], { type: 'text/javascript' });
const worker = new Worker(URL.createObjectURL(blob));
worker.onmessage = event => console.log('worker:', event.data, 'main:', navigator.hardwareConcurrency);

If a check fails, the usual causes are short.

  • The value shows the host's core count. The profile was not loaded or does not carry the expected core count. The value comes from the profile, so check the profile file first.
  • A parallel job is still fast on macOS. macOS does not expose the CPU affinity interface that Linux and Windows use, so Workers run on every core. The reported value still comes from the profile, but parallel timing may not match it. Use Linux or Windows when scaling behavior matters.
  • Workers seem slow. That is expected when a profile claims fewer cores than the host has, because Workers are intentionally constrained to the claimed count.

Scaling is the fifth check, and it depends on the platform. On a host with more cores than the profile claims, a parallel job spread over more Workers than the claimed count should stop speeding up near the claimed count. When several browser instances share one host, each instance is constrained independently by its own profile, so a 4-core profile and an 8-core profile on the same server show different scaling curves that match their respective claims.

A worked example shows the effect. A 32-core build server runs a mobile profile that claims 4 cores. The profile reports 4 in every context and Workers are limited to four physical cores, so a parallel benchmark with 8 Workers stops speeding up near four Workers and shows the scaling curve of a 4-core machine, not the curve the host could manage. Absolute speed still depends on the host's per-core performance, which is usually higher than a phone's.

Good habits keep the result predictable. Use a complete profile instead of overriding one property, choose a profile whose core count fits its platform and device memory, test worker contexts every time, and avoid adjusting the count by hand. If the reported number needs to change, change the profile, because the other device values belong to the same description. Related details live in the guide to navigator property protection, the device memory API guide, the worker planning guide, and the overview of browser fingerprinting.

Keep a short written record of each check so a later change can be compared with it: the profile used, the host platform, the value read in the main thread and in each worker type, and the scaling result on Linux or Windows. A record that also names the browser release and the profile version makes it easy to tell whether a later difference came from the profile, the browser, or the host.

BotBrowser can keep the profile's navigator.hardwareConcurrency consistent in the main thread and in workers, and on Linux and Windows it applies CPU affinity to Worker threads so that parallel benchmark scaling matches the claimed core count. That helps when a larger host runs a profile with fewer cores, because the reported number and the way parallel work scales stay in agreement. It cannot add real CPU cores or change the host's hardware, it does not constrain Workers through CPU affinity on macOS, where parallel timing may not match the profile, and it cannot guarantee how any site interprets the signal.

Practical conclusion

Treat the reported count as a bounded planning input. Read it in every worker context, compare it with the selected profile and platform, and use Linux or Windows when CPU-affinity scaling is part of the test. Change the profile rather than patching one property by hand.

Public sources

#Cpu#Fingerprinting#hardwareConcurrency#Privacy#Browser Fingerprint#Tracking Prevention

This article is part of the guide: What Is a Browser Fingerprint?

Take BotBrowser from research to production

The guides cover the model first, then move into cross-platform validation, isolated contexts, and scale-ready browser deployment.