Distributed systems
Monotonic vs wall-clock time
Wall-clock time tells people when something happened but can jump. Monotonic time only moves forward, so use it to measure how long things take.
Operating systems expose two kinds of clock, and they answer different questions. Wall-clock time answers "what time is it?" It is seconds since the epoch, and it can jump forward or backward when the clock is adjusted. Monotonic time answers "how much time has passed here?" It never goes backward, but it has no meaning across machines or reboots.
Concept
A step is a sudden jump in the wall clock, from NTP correction or a manual change. A monotonic clock counts from an arbitrary starting point, often boot, and only increases.
The big idea
Measuring a duration is different from reading the time. Think of timing a race with a wall clock. If someone resets the wall clock mid-race, the times are wrong. A stopwatch never gets reset that way, so it's the right tool.
The rule is simple. Use monotonic time for durations: timeouts, latencies and lease expiry on the local machine. Use wall-clock time only to show people when something happened.
Walking through it
Subtracting two wall-clock readings measures a duration only if the clock doesn't step in between. If it does, the result can be negative or huge.
// Wrong: breaks if NTP steps the clock during the call.
const start = Date.now()
await callDependency()
const elapsedMs = Date.now() - start // can be negative or huge
// Right: monotonic, immune to clock steps.
const t0 = performance.now()
await callDependency()
const elapsed = performance.now() - t0A runnable check of the monotonic version:
const t0 = performance.now()
await new Promise((resolve) => setTimeout(resolve, 50))
const elapsed = performance.now() - t0
console.log(elapsed >= 50 ? "waited at least 50 ms" : "too short")Output:
waited at least 50 msPython has the same split. time.time() is the wall clock, and time.monotonic() is the stopwatch. A five-second deadline built on time.monotonic() can't be shortened by a clock step.
Monotonic values are not dates. A number such as 84,213 ms means nothing to a human, and it means nothing to a different machine. Convert to wall-clock time only when you show someone a time.
Common mistakes
- Timing timeouts with
Date.now(). A clock step can make the timeout fire early, or never. - Comparing monotonic values across machines. Each machine's counter starts from its own point, so the numbers don't line up.
- Showing monotonic values to users. They aren't calendar times. Convert to wall-clock time for display.
Check yourself
- Which clock should measure how long a database call took? The monotonic clock, because it can't jump during the call.
- Can you compare a monotonic reading from one machine with one from another? No. Each counter starts at its own point, often boot, so the values have no shared meaning.
Key takeaways
- Wall-clock time is for people. Monotonic time is for durations.
- Subtracting wall-clock readings can produce negative or huge durations when the clock steps.
- Use monotonic deadlines for timeouts and leases on one machine.
- Don't compare monotonic values across machines.
Further learning
Kleppmann's lecture on physical time, linked on the clock drift and NTP page, explains why these clocks behave differently.
Next, read about last-write-wins and skew, which shows what happens when wall-clock timestamps decide a conflict.
Related
Clock drift and NTP
Every server's clock drifts, and NTP only corrects it within the limits of the network path. A clock is an estimate with an error bar, not an exact reading.
Time, clocks and ordering
Why wall clocks lie across machines, how skew silently loses writes, and how Lamport clocks, vector clocks, hybrid logical clocks and TrueTime order events you can't time directly.
Observability: logs and traces
Structured logs, distributed tracing and OpenTelemetry, so you can ask new questions of a running system instead of guessing which dashboard to open.