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| Clock | Advances | Affected by NTP | Use for |
|---|---|---|---|
| Wall clock (realtime) | With civil time | Yes, can step back | Timestamps, logging, display |
| Monotonic | Forward only | No (slewed but never steps) | Durations, timeouts, backoff |
| Monotonic raw | Forward, unaffected by slew | No | Precise short measurements |
| Boot time | Monotonic including suspend | No | Cross-restart elapsed time |
| CPU time | Only while running | No | Profiling, 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()| Language | Monotonic API | Wall clock |
|---|---|---|
| Python | time.monotonic(), perf_counter() | time.time() |
| JavaScript | performance.now() | Date.now() |
| Go | time.Since uses monotonic data | time.Now() wall part |
| Java | System.nanoTime() | System.currentTimeMillis() |
| Rust | Instant::now() | SystemTime::now() |
| POSIX C | clock_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 monotonicMeasuring 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.Related
Testing time-dependent code Scheduling, cron and time-based triggers
Last refreshed 2026-09-18.