How performance.now() and hardwareConcurrency Leak Hardware Fingerprints
Timing APIs such as performance.now(), navigator.hardwareConcurrency, and deviceMemory can reveal CPU and memory characteristics. Learn what these signals expose and how to reduce timing-based tracking.
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.
Introduction
Timing APIs do more than help developers benchmark code. They also let websites estimate how fast your hardware is, how many CPU cores are available, and what memory class the device belongs to. Combined with other signals, that is enough to narrow a browser down much more than most users expect.
This guide focuses on the timing surfaces that are easiest to query in practice: performance.now(), navigator.hardwareConcurrency, and navigator.deviceMemory. It explains what these values leak, why naive overrides create new inconsistencies, and what consistent protection requires.
Privacy Impact
Performance timing signals represent a particularly insidious form of fingerprinting because they are deeply tied to physical hardware characteristics that users cannot easily change. Unlike cookies or local storage, which users can clear, timing signals emerge from the fundamental speed at which a device executes JavaScript, renders graphics, and processes data.
Research from institutions including Princeton and KU Leuven has demonstrated that timing-based fingerprinting can distinguish devices with high accuracy. A 2019 study showed that combining performance.now() measurements with hardware concurrency values could correctly identify returning visitors over 80% of the time, even when other identifiers were cleared.
The problem extends beyond individual signals. When a tracker collects navigator.hardwareConcurrency (CPU cores), navigator.deviceMemory (RAM), and microbenchmark execution times together, the combined fingerprint becomes highly distinctive. A device with 6 cores, 8 GB of memory, and a specific JavaScript execution speed profile narrows the identification window significantly. These signals require no user permissions, generate no prompts, and are invisible to the person being tracked.
Technical Background
To understand why performance timing varies between devices, it helps to look at what influences JavaScript execution speed and the APIs that expose hardware information.
Performance.now() and High-Resolution Time
performance.now() returns a high-resolution timestamp measured in milliseconds with microsecond precision. The actual resolution depends on the browser's security mitigations (many browsers reduce precision to prevent Spectre-class attacks), but even reduced precision reveals timing characteristics tied to the CPU's clock speed, cache hierarchy, and instruction pipeline.
When a website runs a microbenchmark, such as iterating a loop 100,000 times or computing SHA-256 hashes, the execution time is directly influenced by the device's hardware. A fast desktop CPU completes the same work faster than a mobile processor, and this difference is measurable and repeatable.
Hardware Concurrency
navigator.hardwareConcurrency returns the number of logical processor cores available to the browser. This value reflects the physical CPU configuration and does not change between sessions. Common values range from 2 (low-end mobile) to 32 or more (high-end workstations). The distribution is not uniform: certain core counts (4, 8, 12, 16) are far more common than others, which means unusual values become strong identifiers.
Device Memory
navigator.deviceMemory returns an approximate amount of device RAM in gigabytes, rounded to the nearest power of two (0.25, 0.5, 1, 2, 4, 8). While the rounding reduces precision, this value still contributes to the overall fingerprint. A device reporting 8 GB of memory combined with 8 cores is a different fingerprint than one reporting 4 GB with 4 cores.
Why These Values Matter Together
Individually, each signal has limited entropy. But combined, they form a hardware profile: a specific execution speed, a specific core count, and a specific memory class. When layered with other fingerprint signals like Canvas, WebGL, and fonts, timing characteristics contribute meaningfully to device identification.
Common Protection Approaches and Their Limitations
VPNs and Proxy Servers
VPNs change your IP address but have zero effect on performance timing. All timing measurements and hardware property queries happen locally within the browser. Two devices behind the same VPN still report entirely different hardware profiles and execution speeds.
Incognito and Private Browsing
Private browsing modes clear cookies and history but do not modify navigator.hardwareConcurrency, navigator.deviceMemory, or the execution speed of the underlying hardware. Your performance fingerprint in incognito is identical to your fingerprint in a normal window.
Browser Extensions
Extensions that attempt to modify hardware properties face fundamental limitations. They can override JavaScript property values by injecting scripts, but this creates inconsistencies that are straightforward to detect:
- If
navigator.hardwareConcurrencyreports 4 cores but a Web Worker microbenchmark shows 8 parallel threads executing simultaneously, the values conflict. - If
navigator.deviceMemoryreports 2 GB butperformance.now()timing for memory-intensive operations is consistent with 16 GB (no garbage collection pauses), the mismatch is apparent. - Extension-injected overrides can be detected by checking property descriptors, prototype chains, or by running code in a fresh context before extensions execute.
Randomization
Some privacy tools randomize timing values by adding noise to performance.now(). While this prevents stable identification, randomly shifting timestamps breaks legitimate performance monitoring and creates its own detectable pattern. A timestamp sequence with artificial jitter looks different from natural hardware variation.
BotBrowser's Engine-Level Approach
BotBrowser takes a fundamentally different approach to performance timing protection. Instead of intercepting API calls or adding noise after the fact, BotBrowser controls timing signals at the browser engine level to produce internally consistent results that match a complete device profile.
Profile-Based Hardware Identity
When you load a fingerprint profile, BotBrowser configures all hardware-related values to match that profile's target device:
chrome --bot-profile="/path/to/profile.enc" \
--user-data-dir="$(mktemp -d)"
This sets navigator.hardwareConcurrency and navigator.deviceMemory to the profile's configured values. These are not JavaScript overrides; they are applied at the engine level before any page code executes. Property descriptor checks, prototype inspection, and fresh-context evaluation all return the same values because the engine itself reports them.
Performance Timing Scale
BotBrowser provides the --bot-time-scale flag to control the apparent execution speed of JavaScript operations:
chrome --bot-profile="/path/to/profile.enc" \
--bot-time-scale=0.92
The flag accepts a float value below 1.0 (typical range: 0.80-0.99). A scale value of 0.92 compresses performance.now() intervals by 8%, reducing timing skew signals that could identify the device. The scaling is applied uniformly across all timing sources, so microbenchmarks, performance.mark(), and performance.measure() all produce internally consistent results that match the same hardware class.
Performance Timing Seeds
Introduced in March 2026, --bot-time-seed (ENT Tier2) provides more granular performance timing protection. While --bot-time-scale compresses timing intervals to reduce skew signals, --bot-time-seed controls the timing distribution across individual browser operations.
chrome --bot-profile="/path/to/profile.enc" \
--bot-time-scale=1.0 \
--bot-time-seed=12345
--bot-time-seed=<integer> accepts an integer seed from 1 to UINT32_MAX (0 disables the feature). Each seed produces a unique, stable performance profile that diversifies timing across many browser operations, ensuring that each instance has a distinct and consistent timing signature.
The seed also covers per-session redistribution of performance.getEntries(), performance.getEntriesByType("navigation"), and performance.timing values, making navigation timing patterns unique to each seed.
This is fully deterministic: the same seed always produces the same timing fingerprint. Combined with --bot-time-scale, you get both macro-level speed control and micro-level timing diversity.
Playwright example:
const { chromium } = require('playwright-core');
(async () => {
const browser = await chromium.launch({
executablePath: '/path/to/botbrowser/chrome',
args: [
'--bot-profile=/path/to/profile.enc',
'--bot-time-scale=1.0',
'--bot-time-seed=12345',
],
headless: true,
});
const context = await browser.newContext({ viewport: null });
const page = await context.newPage();
await page.goto('https://example.com');
// Timing distribution is unique to seed 12345
await browser.close();
})();
Puppeteer example:
const puppeteer = require('puppeteer-core');
(async () => {
const browser = await puppeteer.launch({
executablePath: '/path/to/botbrowser/chrome',
args: [
'--bot-profile=/path/to/profile.enc',
'--bot-time-scale=1.0',
'--bot-time-seed=12345',
],
headless: true,
defaultViewport: null,
});
const page = await browser.newPage();
await page.goto('https://example.com');
await browser.close();
})();
Cross-Signal Consistency
The key advantage of engine-level control is consistency. When BotBrowser reports 4 CPU cores and 4 GB of memory through a profile, the timing characteristics of JavaScript execution also align with that hardware class. Web Workers execute with parallelism consistent with the reported core count. Memory allocation patterns match the reported memory class. There are no contradictions between what the browser reports and how it actually performs.
Deterministic Mode
For testing, research, and CI/CD pipelines, BotBrowser supports fully deterministic timing through the noise seed flag:
chrome --bot-profile="/path/to/profile.enc" \
--bot-time-scale=1.0 \
--bot-noise-seed=42 \
--user-data-dir="$(mktemp -d)"
The same profile, time scale, and noise seed produce identical timing fingerprints across sessions, host machines, and operating systems.
Configuration and Usage
Basic CLI Usage
chrome --bot-profile="/path/to/profile.enc" \
--bot-time-scale=1.0 \
--user-data-dir="$(mktemp -d)"
Playwright Integration
const { chromium } = require('playwright-core');
(async () => {
const browser = await chromium.launch({
executablePath: '/path/to/botbrowser/chrome',
args: [
'--bot-profile=/path/to/profile.enc',
'--bot-time-scale=1.0',
],
headless: true,
});
const context = await browser.newContext({ viewport: null });
const page = await context.newPage();
await page.goto('https://example.com');
// Hardware values and timing signals match the loaded profile
await browser.close();
})();
Puppeteer Integration
const puppeteer = require('puppeteer-core');
(async () => {
const browser = await puppeteer.launch({
executablePath: '/path/to/botbrowser/chrome',
args: [
'--bot-profile=/path/to/profile.enc',
'--bot-time-scale=0.92',
'--bot-noise-seed=42',
],
headless: true,
defaultViewport: null,
});
const page = await browser.newPage();
await page.goto('https://example.com');
await browser.close();
})();
Combined Configuration
For comprehensive protection, combine timing control with other profile settings:
chrome --bot-profile="/path/to/profile.enc" \
--bot-time-scale=1.0 \
--bot-noise-seed=42 \
--proxy-server="socks5://user:pass@proxy:1080" \
--bot-config-timezone="America/New_York" \
--bot-config-locale="en-US"
Verification
After launching BotBrowser with a profile, verify that hardware values and timing are controlled:
// Check hardware properties
console.log('CPU Cores:', navigator.hardwareConcurrency);
console.log('Device Memory:', navigator.deviceMemory, 'GB');
// Measure timing consistency
async function measureTiming() {
const results = [];
for (let i = 0; i < 5; i++) {
const start = performance.now();
for (let j = 0; j < 100000; j++) Math.sqrt(j);
results.push(performance.now() - start);
}
return results;
}
const timings = await measureTiming();
console.log('Timing samples:', timings);
console.log('Variance:', Math.max(...timings) - Math.min(...timings));
What to check:
navigator.hardwareConcurrencymatches the profile's configured core countnavigator.deviceMemorymatches the profile's configured memory- Timing samples show consistent behavior across runs with the same profile and seed
- Public fingerprint testing tools (CreepJS, BrowserLeaks) show no anomalies
Best Practices
-
Always use a complete profile. Hardware values, timing, and other signals must all align. A profile ensures consistency across every fingerprint surface.
-
Use
--bot-time-scaleintentionally. Set the scale to reduce timing skew for the profile's target hardware class. Typical values range from 0.80 to 0.99. Lower values compress timing intervals more aggressively. -
Use deterministic mode for CI/CD. Combine
--bot-noise-seedwith--bot-time-scalefor reproducible results in automated testing pipelines. -
Match proxy geography to profile identity. A profile configured for a US-based device should use a US-based proxy. Timing consistency combined with mismatched geolocation weakens overall fingerprint coherence.
-
Avoid mixing browser extensions that modify timing. BotBrowser handles all timing protection natively at the engine level. Third-party extensions that modify
performance.now()or hardware properties will conflict.
Frequently Asked Questions
Does performance.now() precision differ between browsers?
Yes. Different browsers implement different levels of timestamp precision, partly as a mitigation against Spectre-class side-channel attacks. Chrome, Firefox, and Safari each reduce precision differently. BotBrowser's profile system accounts for the target browser's expected precision characteristics.
Can websites detect that timing values are being controlled?
If timing control is applied inconsistently (for example, modifying performance.now() but not Date.now() or performance.mark()), websites can detect the discrepancy. BotBrowser applies timing scaling uniformly across all timing sources at the engine level, preventing cross-API inconsistencies.
Does --bot-time-scale affect actual page performance?
No. The flag controls how timing values are reported, not how quickly JavaScript actually executes. Pages load and run at full speed; only the measurements reported to JavaScript reflect the scaled timing.
How does hardware concurrency interact with Web Workers?
BotBrowser ensures that the reported navigator.hardwareConcurrency value is consistent with the number of Web Workers that can run in parallel. The profile manages both values together.
Can I use different time scales for different profiles?
Yes. Each browser instance uses its own --bot-time-scale value. You can run multiple instances simultaneously with different profiles and different timing characteristics.
What about SharedArrayBuffer timing?
SharedArrayBuffer combined with Atomics can be used as a high-resolution timer. BotBrowser's engine-level timing control applies to shared memory timing channels as well, maintaining consistency across all timing measurement methods.
Do timing fingerprints survive browser updates?
On unprotected browsers, yes. The timing fingerprint is tied to hardware, not browser version. BotBrowser profiles are versioned, so you can maintain consistent timing characteristics across profile updates.
What is the difference between --bot-time-seed and --bot-time-scale?
They serve complementary purposes. --bot-time-scale compresses performance.now() intervals (accepts a float below 1.0, typical range 0.80-0.99) to reduce timing skew signals. --bot-time-seed controls the timing distribution across individual browser operations, producing a unique per-operation timing profile for each seed value. You can use both together: --bot-time-scale sets the macro-level timing compression, while --bot-time-seed adds micro-level timing diversity within that profile.
Summary
Performance timing APIs expose hardware characteristics that serve as stable tracking signals. performance.now(), navigator.hardwareConcurrency, and navigator.deviceMemory together form a hardware identity that persists across sessions and survives common privacy measures. BotBrowser controls all of these signals at the browser engine level through its profile system, --bot-time-scale for timing interval compression, and --bot-time-seed for per-operation timing diversity. Together, these flags ensure internally consistent results that match the target device identity. See all engine-level protections on the features page. For related protection, see Canvas fingerprint protection, WebGL fingerprint control, and frame rate control.
Related Articles
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.