
What Nobody Tells You About hardwareConcurrency: Why the Browser Is Silently Throttling Your Parallel Logic
Hard-won insights into why modern browsers lie about your CPU core count to prevent fingerprinting, and how it silently caps the performance of your multi-threaded Web Workers.
Your browser is straight-up lying to you about how powerful your computer is.
If you’ve ever opened the console and typed navigator.hardwareConcurrency, you likely saw a nice, even number like 4, 8, or 16. You probably took that number, shoved it into a loop, and spawned exactly that many Web Workers to handle some heavy-duty image processing or data crunching. But here’s the kicker: that number is often a polite fiction. To prevent advertisers and bad actors from tracking you, browsers are actively sabotaging your ability to scale parallel logic.
The Promise of hardwareConcurrency
The theory is simple. navigator.hardwareConcurrency returns the number of logical processors available to the user's device. In a perfect world, this is the magic number for peak performance.
If you spawn fewer workers than cores, you’re leaving power on the table. If you spawn more, you run into context-switching overhead where the CPU spends more time "swapping" tasks than actually doing them.
The "standard" way we were taught to use it looks something like this:
// The naive approach
const logicalCores = navigator.hardwareConcurrency || 2;
const workers = [];
for (let i = 0; i < logicalCores; i++) {
const worker = new Worker('processor.js');
workers.push(worker);
}
console.log(`Spawning ${workers.length} workers for our heavy lifting.`);It looks clean. It looks professional. And on a modern MacBook Pro or a high-end Ryzen desktop, it’s probably bottlenecking your app by 50% or more.
Why Browsers Started Lying
The culprit is Browser Fingerprinting.
Every tiny detail your browser reveals—your screen resolution, your installed fonts, your GPU model—is a piece of a puzzle. If I know you have exactly 36 logical cores (hello, Threadripper users), that makes you very unique.
To combat this, browser vendors decided that privacy is more important than your web app's ability to max out the CPU.
The Safari "Cap"
WebKit (Safari) was the first to get aggressive. For a long time, Safari on iOS and even macOS would frequently report a maximum of 2 or 4, regardless of how many cores you actually had. They effectively "clamped" the value. If you have a 10-core M2 chip, Safari might still tell your code "I've only got 4 cores, boss."
The Firefox "Privacy Toggle"
If a user enables "Resist Fingerprinting" (RFP) in Firefox, navigator.hardwareConcurrency is hard-coded to return exactly 2. It doesn't matter if you're running a server-grade CPU with 128 threads; your code sees a dual-core machine from 2010.
The Chrome "Rounding"
Chrome is generally more honest, but they have experimented with "bucketing" values—rounding your core count to the nearest common power of two to make you blend in with the crowd.
The Performance Penalty
When the browser lies, your parallel logic suffers. Imagine you are building a video encoder in the browser using WebAssembly. If the browser reports 4 cores but the machine has 16, your encoding job will take four times longer than necessary.
Conversely, if the browser reports a high number but is actually throttling the execution of background tabs (which Chrome and Edge do aggressively to save battery), your workers might start fighting for resources, leading to "jank" and UI lag.
Can We Detect the Lie?
You can't "force" the browser to tell the truth, but you can be smarter about how you distribute work. Instead of blindly trusting a single integer, you can run a quick "stress test" or use a more conservative scaling strategy.
Here is a more robust way to initialize a worker pool that accounts for potential throttling:
/**
* A slightly smarter way to determine worker count.
* We cap it to avoid overwhelming the system,
* and provide a fallback for weird browser behavior.
*/
function getOptimalWorkerCount() {
const hardware = navigator.hardwareConcurrency || 2;
// If we are on a high-end machine, we might want to
// reserve one core for the Main Thread to keep the UI buttery smooth.
const count = hardware > 4 ? hardware - 1 : hardware;
// Set an upper limit. Spawning 64 workers in a browser
// is usually a recipe for a crashed tab.
return Math.min(count, 12);
}
const poolSize = getOptimalWorkerCount();
console.log(`Will attempt to use ${poolSize} threads.`);The "Adaptive" Approach
If your app's performance is mission-critical, stop treating hardwareConcurrency as a constant. Treat it as a suggestion.
A sophisticated approach involves measuring the time it takes for a "canary" task to complete. If you spawn 4 workers and they are all finishing tasks instantly, try bumping it up to 6. If the completion time spikes, scale back.
// Pseudo-code for an adaptive worker strategy
class WorkerManager {
constructor() {
this.limit = navigator.hardwareConcurrency || 2;
this.activeWorkers = [];
}
monitorPerformance() {
// If workers are struggling (latency > threshold),
// stop spawning new ones even if hardwareConcurrency says we can.
if (this.currentLatency > 100) {
console.warn("CPU Throttling detected by browser or OS. Reducing load.");
this.throttleDown();
}
}
}The "Gotchas" You Need to Know
1. Efficiency vs. Performance Cores: On newer chips (like Apple Silicon or Intel’s 12th+ Gen), not all cores are equal. hardwareConcurrency counts both the "Performance" cores and the "Efficiency" cores. If your workers get scheduled on the efficiency cores, they will be significantly slower.
2. Low Power Mode: When a laptop or phone enters Low Power Mode, the browser often ignores your requests for high-performance parallelism or heavily throttles the hardwareConcurrency value to save juice.
3. Background Throttling: If your tab isn't focused, most browsers will limit your workers to ~1% of CPU time anyway. No amount of core-counting will fix that.
Final Thoughts
We like to think of the web as a transparent platform where we have direct access to the hardware via clever APIs. The reality is that we are working inside a highly regulated sandbox.
The next time your multi-threaded logic feels sluggish on a high-end machine, don't just profile your code—check if the browser is lying to your face. Use navigator.hardwareConcurrency as a starting point, but always build in a ceiling and a floor. Your users' privacy is important, but so is their time. Don't let a "fake" core count turn your high-performance app into a slideshow.

