Monotonic clocks and elapsed time

Why wall clock time is the wrong tool for measuring duration, what a monotonic clock guarantees, and how to measure latency and timeouts without NTP jumps wrecking the result.

The wall clock can move backwards

The system wall clock is continuously corrected by NTP, by virtualisation hosts and by manual changes. It can jump forward, and occasionally backwards. Using it to measure elapsed time therefore produces negative durations and timeouts that never fire.

const start = Date.now();
await work();
const elapsed = Date.now() - start;   // can be negative after an NTP step

// the correct tool
const t0 = performance.now();
await work();
const ms = performance.now() - t0;    // monotonic, sub-millisecond
ClockAdvancesAffected by NTPUse for
Wall clock (realtime)With civil timeYes, can step backTimestamps, logging, display
MonotonicForward onlyNo (slewed but never steps)Durations, timeouts, backoff
Monotonic rawForward, unaffected by slewNoPrecise short measurements
Boot timeMonotonic including suspendNoCross-restart elapsed time
CPU timeOnly while runningNoProfiling, CPU budgets

The API in each language

import time

t0 = time.monotonic()          # forward-only, arbitrary origin
do_work()
print(f"took {time.monotonic() - t0:.3f}s")

print(time.perf_counter())     # highest resolution monotonic clock
print(time.process_time())     # CPU time used by this process
print(time.time())             # wall clock — do NOT use for durations

def with_deadline(fn, seconds):
    deadline = time.monotonic() + seconds     # deadline on the monotonic clock
    while time.monotonic() < deadline:
        fn()
LanguageMonotonic APIWall clock
Pythontime.monotonic(), perf_counter()time.time()
JavaScriptperformance.now()Date.now()
Gotime.Since uses monotonic datatime.Now() wall part
JavaSystem.nanoTime()System.currentTimeMillis()
RustInstant::now()SystemTime::now()
POSIX Cclock_gettime(CLOCK_MONOTONIC)clock_gettime(CLOCK_REALTIME)
start := time.Now()
doWork()
// Sub uses the monotonic reading when both values have one
fmt.Println(time.Since(start))

// mixing a wall-clock-only time into the subtraction loses the monotonic part
wall := time.Unix(0, start.UnixNano())
fmt.Println(start.Sub(wall))   // wall-clock semantics, not monotonic

Measuring things correctly

  • Measure elapsed time with a monotonic clock; attach wall-clock timestamps only for logging.
  • A monotonic clock has an arbitrary origin — never serialise it or compare it across processes.
  • Monotonic clocks on different machines are unrelated; use wall clock (with sync) for cross-host ordering.
  • Coarse clocks tick in multiples of a few milliseconds; a sub-tick measurement may read as zero.
  • For distributed traces, record wall-clock start and end plus a monotonic duration, and be explicit about which is authoritative.
# a timeout that cannot be fooled by a clock step
def run_with_timeout(call, timeout_s):
    deadline = time.monotonic() + timeout_s
    while True:
        remaining = deadline - time.monotonic()
        if remaining <= 0:
            raise TimeoutError
        if call(min(remaining, 0.25)):    # never block longer than the deadline
            return
⚠️
Never log a monotonic value as if it were a timestamp. Its origin is arbitrary — often boot time — and a log with 18734.5 next to real ISO timestamps is worse than no value at all.

FAQ

Can I use a monotonic clock for a cache TTL?
Yes, and you should. A TTL computed from the wall clock can extend or shorten when the clock steps. Use monotonic time for anything measured from process start.
Does a monotonic clock keep counting during suspend?
It depends. CLOCK_MONOTONIC typically stops during suspend while CLOCK_BOOTTIME includes it. Check which one your language's API maps to if that matters.

Testing time-dependent code Scheduling, cron and time-based triggers

Last refreshed 2026-09-18.