From clocks to hardware signatures
Math tests and pixel hashes tell you about the software stack; timing tells you about the hardware underneath it. A script runs a fixed GPU draw or a WASM SIMD kernel (a CPU-heavy compute task) and measures how long it actually takes on the clock. A real discrete GPU finishes a shader far faster than Chrome's SwiftShader software renderer (which fakes graphics work on the CPU when no GPU is present), and a real CPU's cache hierarchy produces a Prime+Probe latency curve - a known timing pattern from cache attacks - that an emulated environment does not. Academic side-channel work (cache attacks, GPU timing) showed these measurements can even infer what other tabs or processes are doing - which is why browsers coarsened performance.now() and gated SharedArrayBuffer behind cross-origin isolation after Spectre (a 2018 CPU vulnerability that abused precise timers).
Why headless browsers fail timing checks
Headless browsers on GPU-less servers have no graphics chip, so they fall back to software rendering - drawing on the CPU instead. A draw that takes ~2 ms on a real GPU might take 50–200 ms in SwiftShader - a giant, obvious tell. Anti-bots that pair a canvas probe with a timing probe can catch a replayed canvas hash (a saved-and-reused image fingerprint): the pixels look real but the render took software-renderer time. The same logic flags WASM workloads that run at emulated speed. There is no JavaScript fix; you need real (or GPU-accelerated) hardware to produce real timings.
Why timing is hard to fake — and what works
Timing side-channels are powerful precisely because they read the real hardware underneath the browser. Even with performance.now() resolution deliberately coarsened to defend against Spectre, the relative cost of operations - cache hits versus misses, JIT warm-up (the moment the engine compiles hot JavaScript to fast machine code), GPU draw timing - reflects the actual CPU and memory you are running on. You cannot convincingly fake those ratios from JavaScript, because the code you add to fake them also takes measurable time.
So the practical answer is to run on hardware whose timing profile matches the identity you present, rather than to spoof clocks. A datacentre VM running headless Chrome behind a residential User-Agent has a timing signature that does not match a real consumer laptop. Residential and real-device infrastructure - the kind a managed web scraping API runs behind - produces genuine timing characteristics instead of trying to forge them.
Timer quantisation and the grid it creates
High-resolution timers were reduced in precision across all major browsers after Spectre, and the coarsening itself became an observable. performance.now() no longer returns arbitrary floating-point values: results are clamped to a quantisation grid whose step depends on the browser, its version, and whether the document is cross-origin isolated. Restoring finer resolution requires the correct COOP and COEP headers, so the effective step size is a readable property of the page context.
Two checks follow from this. First, the observed step is itself a fingerprinting input, narrowing the browser and version independently of any declared string. Second, and more usefully, every PerformanceEntry timestamp - resource loads, marks, measures, navigation milestones - is generated by the same clock and must land on the same grid. A timestamp sitting between grid points was not produced by the clock it claims to come from, which is how a modified or re-implemented timer becomes visible.
This is where timing analysis stops being purely about measurement and becomes a clock coherence problem: the browser exposes several time sources that must reconcile with each other, and altering one without the others produces contradictions that are far easier to act on than any single duration measurement.
What durations reveal about the machine and the stack
With a usable clock, elapsed time becomes a proxy for hardware and software characteristics that no API reports directly.
- Compute profile - the ratio between a floating-point-heavy loop and an integer-heavy one varies by CPU microarchitecture. Ratios are used rather than absolute times because they survive differences in machine load.
- JIT warm-up - the shape of the curve as a hot function is optimised is characteristic of the engine and its tier-up thresholds, and differs between engines running the same code.
- GPU pipeline latency - the delay between issuing draw calls and a readback completing separates hardware rendering from software rasterisation, corroborating what WebGL fingerprinting reports as a renderer string.
- Storage and network tiers - cache hit and miss latencies group devices into broad performance classes, and connection round-trip estimates are quantised in ways that can be checked against the reported effective connection type.
The recurring pattern is corroboration rather than identification. Timing rarely names a device, but it constantly tests whether declared capabilities are plausible. A session reporting a high-end discrete GPU while its rendering latency profile matches a software rasteriser has produced a contradiction between what it says and what it does - and unlike a string, a latency profile is a property of the machine actually executing the work.
The corollary for anyone running browsers at scale is that hardware honesty is cheaper than hardware claims. An environment with real GPU access and real system resources produces timing profiles that match its declared capabilities without any effort, which is one reason managed browser infrastructure sidesteps a class of problem that headless containers on shared CPU inherit by default.
