DEV Community

Cover image for Your Clock Can Go Backward—Use the Right One for Durations
Luciano Menezes
Luciano Menezes

Posted on

Your Clock Can Go Backward—Use the Right One for Durations

A request starts at 10:00:00.900 and finishes 200 ms later.

Your latency log says -800 ms.

That sounds impossible until the machine corrects its clock between the two reads. Then ordinary code turns an adjustable wall clock into a broken stopwatch.

The subtraction that works—until it does not

This is probably hiding in one of your metrics helpers:

async function timed(operation) {
  const startedAt = Date.now();
  const result = await operation();

  return {
    result,
    durationMs: Date.now() - startedAt,
  };
}
Enter fullscreen mode Exit fullscreen mode

Most of the time, this reports a sensible number. That is what makes it dangerous.

Date.now() answers a calendar question: how many milliseconds have passed since the Unix epoch according to this machine? The answer must remain comparable with logs, users, and other computers, so synchronization software is allowed to correct it. A correction can change its rate, jump it forward, or move it backward.

If either correction lands between the two calls, the subtraction measures the clock adjustment as if it were work.

A timestamp is a coordinate. A duration is a distance. They need different instruments.

Your computer already has two kinds of clock

Operating systems expose several clocks, but two mental buckets cover most application code:

Question Clock JavaScript example
“When did this happen?” Wall clock Date.now(), new Date()
“How long did this take?” Monotonic clock performance.now()

A wall clock follows civil time. It has an epoch and can be serialized as an ISO timestamp. A monotonic clock starts at an arbitrary origin and promises that later readings will not be lower than earlier readings.

wall:       1000 ── 1001 ── 0998 ── 0999   clock correction
monotonic:    40 ───── 41 ───── 42 ───── 43
Enter fullscreen mode Exit fullscreen mode

On Linux, CLOCK_REALTIME is settable wall time. CLOCK_MONOTONIC cannot be set and does not make discontinuous jumps backward, although gradual frequency adjustment can affect its rate. Linux even has CLOCK_BOOTTIME for a monotonic counter that includes suspended time.

Three properties are easy to confuse:

  • Resolution: how small a change the clock can represent.
  • Accuracy: how close it is to an external reference.
  • Monotonicity: whether readings can go backward.

A microsecond-resolution wall clock can still jump. A coarse monotonic clock can still be the correct stopwatch. MDN says browsers may expose Performance API timestamps at 5 µs in isolated contexts or coarsen them to 100 µs for privacy. Both modes remain monotonic.

A negative duration once broke DNS

At midnight UTC on January 1, 2017, a leap second exposed this exact assumption inside Cloudflare’s RRDNS service.

RRDNS measured upstream DNS resolver performance. During the leap, some elapsed values became negative. Those values entered a weighted-selection calculation and eventually reached Go’s rand.Int63n, which panics when its argument is negative.

The blast radius was small in percentage terms but enormous in context: Cloudflare reported about 0.2% of DNS queries affected at peak, under 1% of HTTP requests, across a small number of machines in 102 data centers. The worst-hit machines were patched within 90 minutes; the worldwide fix finished at 06:45 UTC.

Why did redundant machines fail together? The leap second was a correlated trigger. The same rare assumption existed in many replicas, and the external event arrived everywhere at once.

Go changed its time model after this class of failure. Its current time.Now() value may carry both wall and monotonic readings; subtraction uses the monotonic part when both operands have it. The design proposal estimated roughly 30% of time.Now calls were being used to measure elapsed time.

This was not exotic timekeeping trivia. It was an API footgun with production evidence.

Fix the stopwatch, then fix the deadline

For a duration inside one browser context or Node.js process, use performance.now():

async function timed(operation) {
  const startedAt = performance.now();
  const result = await operation();

  return {
    result,
    durationMs: performance.now() - startedAt,
  };
}
Enter fullscreen mode Exit fullscreen mode

The Performance API uses a stable monotonic clock whose origin is performance.timeOrigin. Node’s implementation returns high-resolution milliseconds from process start. If you need integer nanoseconds in Node, process.hrtime.bigint() serves the same interval-measurement job.

The same rule improves timeouts. Suppose an HTTP handler has 250 ms for authentication, a database query, and an upstream request. Giving each step a fresh 250 ms timeout turns one budget into 750 ms.

Calculate one monotonic deadline and pass the remaining budget down:

async function fetchWithDeadline(url, deadlineMs) {
  const remainingMs = Math.max(0, deadlineMs - performance.now());
  if (remainingMs === 0) throw new Error('request budget exhausted');

  const controller = new AbortController();
  const timer = setTimeout(() => controller.abort(), remainingMs);

  try {
    return await fetch(url, { signal: controller.signal });
  } finally {
    clearTimeout(timer);
  }
}

const deadlineMs = performance.now() + 250;
await authenticate(deadlineMs);
await queryDatabase(deadlineMs);
await fetchWithDeadline('https://inventory.internal', deadlineMs);
Enter fullscreen mode Exit fullscreen mode

A timeout belongs to one operation; a deadline preserves one budget across the whole call tree.

In real code, clamp every remaining budget to zero, stop before starting work with no budget left, and record which stage consumed it. That turns “request timed out” into “database queue used 187 of 250 ms.”

The boundary: never serialize a monotonic reading

A monotonic value is meaningful only relative to its own clock origin. Process A’s 5421.3 and process B’s 5421.3 are not the same instant. A restart creates another origin. Serialization often strips monotonic information deliberately; Go’s time package does exactly that.

Use this decision table:

Need Use
Request latency, retry delay, local timeout Monotonic time
Log timestamp, invoice date, calendar event Wall time
Persisted expiration such as expires_at Authoritative wall time, usually server-side
Ordering events across machines Sequence, logical clock, or database order—not client wall time
Distributed lease safety Consensus/authority plus fencing tokens—not clock subtraction alone

Monotonic clocks also differ around suspend. Linux CLOCK_MONOTONIC excludes sleep, while CLOCK_BOOTTIME includes it; Go documents that monotonic elapsed time may exclude suspend on some systems. Ask whether “five minutes” means five minutes of active process time or five minutes experienced by a user.

NTP does not remove these choices. Leap smearing avoids a one-second step by changing clock rate over a window, but smear strategies are not universal. Google’s Go proposal describes a 20-hour smear at 99.9986% speed; Meta describes its own 17-hour smear.

Make time testable and observable

Production telemetry needs both clocks, each doing its own job:

const observedAt = new Date().toISOString();
const startedAt = performance.now();
Enter fullscreen mode Exit fullscreen mode

await handleRequest();

logger.info({
observedAt,
durationMs: performance.now() - startedAt,
});


The wall timestamp lets you correlate services. The monotonic duration keeps the measurement immune to a local clock correction. Monitor host clock offset separately; do not bake it into request latency.

Wrap both clock reads behind injectable functions in business logic. Tests can then step wall time backward, jump it forward, and advance monotonic time independently. That catches expiry and timeout assumptions without changing the developer laptop’s clock.

Avoid the tempting fallback <code>Math.max(0, Date.now() - start)</code>. Cloudflare used a defensive negative check as an emergency mitigation, but clamping merely hides the wrong measurement. Use the right clock and keep guards for impossible values as alarms.

## One rule worth remembering

**Wall time tells you when. Monotonic time tells you how long.**

Use <code>Date</code> for timestamps people and systems exchange. Use <code>performance.now()</code>, <code>process.hrtime.bigint()</code>, or your runtime’s monotonic API for latency, budgets, and local deadlines. Do not send monotonic readings across a process boundary.

Which piece of your codebase still subtracts two wall-clock timestamps—and what would happen if the second one were smaller?
Enter fullscreen mode Exit fullscreen mode

Top comments (4)

Collapse
 
frank_signorini profile image
Frank

That -800ms latency log is such a classic example of why CLOCK_REALTIME

Collapse
 
luciano655 profile image
Luciano Menezes

True
I won’t make the same mistake again, since I already made it once in production haha

Collapse
 
nazar-boyko profile image
Nazar Boyko

Passing one monotonic deadline down the call tree instead of a fresh timeout per step is a nice trick. One thing I've wondered about: if the machine sleeps in the middle of an await, performance.now() keeps counting the suspended time, so the deadline quietly overshoots even though no real work happened. Do you just accept that for request budgets, or is there a clean way to exclude sleep the way CLOCK_MONOTONIC does?

Collapse
 
raju_dandigam profile image
Raju Dandigam

Good call-out on separating stopwatch time from wall-clock time. The bug I keep seeing is teams fixing the negative duration calculation but still deriving per-step timeouts from Date.now(), which means retries inherit a clock that can jump underneath them. Passing a monotonic deadline down the whole call chain is the part that keeps the fix from turning into another metric-only patch.