Skip to content
On this page

Clock coherence detection compares the several independent time sources a browser exposes and checks that they describe the same reality. A page can read the wall clock through Date.now(), a monotonic high-resolution clock through performance.now(), an absolute navigation origin through performance.timeOrigin, and a full set of load milestones through the Navigation Timing API. These are backed by different underlying counters, but they measure one session on one machine, so they must reconcile. When they do not, something has changed one clock without changing the others.

Quick facts

Core identityperformance.timeOrigin + performance.now() should reconstruct Date.now()
Typical real driftA few milliseconds; large or growing gaps are anomalous
Rate checkWall clock and monotonic clock must advance at the same rate over an interval
Cross-API checkNavigation Timing Level 1 and Level 2 must describe the same page load
Grid checkPerformanceEntry timestamps share the quantisation grid of performance.now()

The clocks a browser exposes

Three distinct time sources are visible to any page, and each answers a different question:

  • The wall clock - Date.now() and new Date() report civil time from the operating system. It can jump backwards or forwards when the system clock is adjusted, and it carries the timezone offset.
  • The monotonic clock - performance.now() counts milliseconds since the document's time origin and never goes backwards. It is deliberately independent of system clock changes, which is what makes it useful as a control.
  • The navigation origin - performance.timeOrigin is an absolute wall-clock timestamp for the moment the monotonic clock started, which is what ties the other two together.

The identity that must hold is straightforward: timeOrigin + now() should reconstruct Date.now() to within a few milliseconds. On an unmodified browser the residual is small and stable. Adjusting the reported wall clock without adjusting the origin, or vice versa, breaks the identity immediately, and the size of the break is often exactly the adjustment that was applied.

Rate, grid, and cross-API agreement

Beyond the single reconciliation, three further checks add independent confirmation.

Rate agreement. Sample both clocks, wait, and sample again. The elapsed wall time and the elapsed monotonic time should match closely. A one-off offset survives this check; a clock that runs at a different rate does not. Because the two counters are read through different code paths, keeping both rates aligned requires changing both consistently.

Grid coherence. Browsers deliberately coarsen high-resolution timers to limit timing attacks, so performance.now() values land on a quantisation grid whose resolution depends on the browser, its version, and whether cross-origin isolation is active. Every PerformanceEntry timestamp - resource loads, marks, measures - is produced by the same clock and must land on that same grid. A value that sits off the grid was produced by something other than the clock it claims to come from.

Cross-API agreement. Navigation Timing exists in two generations that both describe the same page load: the legacy performance.timing object with absolute epoch timestamps, and the modern PerformanceNavigationTiming entry with values relative to timeOrigin. Converting between them must produce the same milestones in the same spec-defined order. This is a straight cross-API coherence check applied to time.

Why clocks get adjusted, and what it costs

Clocks are adjusted for two ordinary reasons. The first is locale consistency: an exit IP in one region and a system timezone from another produces a timezone and IP mismatch, and the obvious fix is to change the reported timezone. The second is throughput: scripts that shorten timers to make a page finish faster inevitably touch time.

Both are safe when done at the right layer and expensive when done at the wrong one. Changing the timezone at the operating-system or engine level moves Date, Intl, and timeOrigin together, so every check above continues to reconcile. Overriding Date in page JavaScript moves one clock and leaves performance untouched, which breaks the reconstruction identity, and also leaves a non-native Date constructor for native function integrity checks to find.

The general rule matches the rest of this cluster: time is a property of the environment, so it should be set where the environment is defined. Aligning the exit IP, the system timezone, and the accepted languages at the infrastructure layer removes the reason to touch the clock from JavaScript at all - which is how a managed web data API keeps locale coherent without creating a timing anomaly in the process.

Code example

javascript
// Four clock checks. A real browser passes all four.

// 1. Reconstruction: timeOrigin + now() should rebuild the wall clock
const drift = Math.abs(performance.timeOrigin + performance.now() - Date.now());

// 2. Rate: both clocks must advance together over a real interval
const w0 = Date.now(), m0 = performance.now();
setTimeout(() => {
  const wallElapsed = Date.now() - w0;
  const monoElapsed = performance.now() - m0;
  const rateSkew = Math.abs(wallElapsed - monoElapsed);

  // 3. Grid: PerformanceEntry timestamps share performance.now()'s quantisation
  const entries = performance.getEntriesByType('resource').slice(0, 20);
  const gridStep = 0.1;   // varies by browser and cross-origin isolation state
  const offGrid = entries.filter(e => {
    const r = e.startTime / gridStep;
    return Math.abs(r - Math.round(r)) > 1e-6;
  }).length;

  // 4. Cross-API: legacy and modern navigation timing describe one load
  const legacy = performance.timing;
  const modern = performance.getEntriesByType('navigation')[0];
  const apisAgree = !legacy || !modern ||
    Math.abs((legacy.responseStart - legacy.navigationStart) - modern.responseStart) < 2;

  console.log({ drift, rateSkew, offGrid, apisAgree });
}, 1000);

Next in Coherence and lie detection · 8 of 8

Finally: what a fingerprint test score does and does not tell you.

How to Read a Fingerprint Test Score

Related terms

Concept map

Concept map

How Clock Coherence Detection connects

The terms most directly tied to this one. Hover a node to see its neighbours, click to preview, drag to rearrange.

0 terms · 0 connections
You are here · Anti-Bot
Building map…

Frequently asked questions

How much drift between Date.now() and performance.timeOrigin plus performance.now() is normal?

A few milliseconds. The two values are read at slightly different instants and the wall clock may be adjusted by NTP during a long session, so an exact match is not expected. Consistent drift of tens or hundreds of milliseconds, or drift that grows over the life of the page, indicates the clocks are not being driven by the same underlying source.

Why does overriding Date in JavaScript break clock coherence?

Because it moves only one of the browser clocks. The performance timeline, the navigation timing entries, and timeOrigin are all produced by a separate monotonic source that a Date override does not touch. The result is a browser whose wall clock and monotonic clock no longer reconcile, plus a Date constructor that is no longer a native function.

What is the right way to run a browser session in a different timezone?

Set it at the environment level - the operating system timezone, or an engine-level setting the browser reads at startup. Everything downstream, including Date, Intl, timeOrigin, and the navigation timeline, is then derived from one consistent source. The goal is for the timezone to match the exit IP genuinely, rather than to have JavaScript report a value the rest of the environment contradicts.

Last updated: 2026-07-30 · Facts last verified: 2026-07-30