Performance Timing Fingerprinting: How Hardware Speed Tracks You
Performance.now() precision, hardware concurrency, and timing patterns reveal your device. Learn how timing-based fingerprinting works.
Introduction
Timing-based fingerprinting exploits the fact that different hardware executes operations at different speeds. By measuring the time taken for specific computations, scripts can infer CPU speed, core count, and even distinguish between hardware generations. Combined with performance.now() precision and SharedArrayBuffer timing, these techniques create a hardware-specific timing profile.
How Timing Fingerprinting Works
performance.now() Precision
The performance.now() API provides high-resolution timestamps:
const start = performance.now();
// Perform some operation
const end = performance.now();
const duration = end - start; // Microsecond precision
Browser vendors have reduced the precision of performance.now() to mitigate Spectre-type attacks:
| Browser | Precision |
|---|---|
| Chrome | 5 microseconds (with Site Isolation) |
| Firefox | 1 millisecond (reduced from 5us) |
| Safari | 100 microseconds |
The precision level itself is a browser fingerprinting signal.
Computation Timing
By timing standardized operations, scripts can profile CPU speed:
function benchmarkCPU() {
const start = performance.now();
// Standardized workload
let sum = 0;
for (let i = 0; i < 1000000; i++) {
sum += Math.sqrt(i) * Math.sin(i);
}
return performance.now() - start;
}
Different CPUs produce measurably different execution times for the same workload.
Worker Thread Timing
Using Web Workers for parallel timing reveals the actual core count:
async function measureCores() {
const timings = [];
for (let workers = 1; workers <= 16; workers++) {
const start = performance.now();
const promises = [];
for (let i = 0; i < workers; i++) {
promises.push(new Promise(resolve => {
const worker = new Worker('benchmark-worker.js');
worker.onmessage = () => resolve();
worker.postMessage('start');
}));
}
await Promise.all(promises);
timings.push({
workers,
time: performance.now() - start,
});
}
// Timing increases non-linearly when workers > physical cores
return timings;
}
When the number of workers exceeds the physical core count, execution time increases sharply. This reveals the actual core count even if navigator.hardwareConcurrency is spoofed.
SharedArrayBuffer Timing
SharedArrayBuffer enables shared memory between workers, providing a high-precision timing side channel:
const sab = new SharedArrayBuffer(4);
const arr = new Int32Array(sab);
// Worker increments the counter continuously
// Main thread reads it to measure precise time intervals
This technique can achieve sub-microsecond timing precision, bypassing performance.now() restrictions.
Detection Implications
Timing fingerprinting is used to:
- Verify hardware claims - Does the CPU speed match the claimed
hardwareConcurrency? - Detect virtual machines - VMs often have timing anomalies
- Identify headless mode - Some headless configurations produce different timing profiles
- Correlate sessions - Consistent timing patterns link sessions to the same hardware
How BotCloud Handles Timing
BotCloud manages timing-based fingerprinting through:
- Performance.now() calibration aligned with the profile's claimed hardware
- Consistent timing patterns across sessions with the same profile
- Worker timing that does not reveal the actual server hardware
- Timer precision matching normal browser behavior
Best Practices
- Match performance characteristics to claimed hardware - An 8-core profile should show 8-core parallelism
- Avoid impossibly fast or slow timing - Timing should fall within normal ranges for the claimed CPU
- Verify SharedArrayBuffer behavior - If available, its timing should be consistent with performance.now()
- Test with timing-based detection tools to verify consistency