67

Wall clock lies: ordering is not its job

#
  • pattern
  • established
  • fragment

Scope. Logical clocks solve one problem — the “happened before” relation. Durations and deadlines are measured with monotonic clocks, domain and human time stays with the wall clock, and ownership of a resource is proven by a generation number (66), not by time at all. The card’s claim is about ordering across nodes, not that the wall clock is unnecessary.

Primary sources: Leslie Lamport — Time, Clocks, and the Ordering of Events in a Distributed System

NTP drift, leap smearing, VM pauses: System.currentTimeMillis() on two machines is not comparable, and “last write wins by timestamp” silently loses writes. Ordering is done with Lamport clocks / HLC / version vectors; wall clock is only for humans (logs, metrics) and for TTLs with a margin.

Three clocks that get confused. Wall clock (currentTimeMillis, CLOCK_REALTIME) answers “when, by the calendar”: logs, domain dates, TTLs with a margin. It jumps — forward and backward — on NTP correction. Monotonic clock (nanoTime, CLOCK_MONOTONIC) doesn’t jump and is therefore the only one fit for durations, timeouts and deadlines — but it is not comparable across machines and resets on restart, so storing it is pointless. Logical clocks (Lamport, vector, HLC) are the only source of the “happened before” relation between nodes. The typical bugs are exactly these substitutions: a duration computed from the wall clock goes negative during a correction, and “ordering” by the monotonic clock is meaningless outside the process. A separate layer is the fencing token (66): there you need not the order of events but the owner’s generation number.

Code example (fragment)
fragment
Python · HLC
# physical time + logical counter, monotonicity guaranteed
@dataclass(frozen=True, order=True)
class HLC:
    wall_ms: int
    logical: int

    def tick(self, now_ms: int) -> "HLC":
        if now_ms > self.wall_ms:
            return HLC(now_ms, 0)
        return HLC(self.wall_ms, self.logical + 1)   # clock went backwards: bump the counter

    def recv(self, other: "HLC", now_ms: int) -> "HLC":
        m = max(self.wall_ms, other.wall_ms, now_ms)
        l = max(self.logical if m == self.wall_ms else -1,
                other.logical if m == other.wall_ms else -1) + 1
        return HLC(m, l)
Java
// review smell: comparing times recorded by different nodes
if (eventA.timestamp().isBefore(eventB.timestamp())) { ... } // ← flag it if A and B
                                                             //   were written by different nodes

Google TrueTime is the exception that proves the rule: Spanner buys clock comparability with atomic clocks and an explicit uncertainty interval (commit-wait). If you don’t have atomic clocks, you don’t have comparable timestamps.

Don’t mix the three clocks: wall clock for calendar time and display; monotonic clock for durations, timeouts and deadlines; HLC or version vectors for causal order. And last-write-wins is not always a bug: it is a legitimate convergence policy if losing a concurrent update is a deliberate decision rather than a surprise.

Last technical review: 2026-08-04 · Reference · 112 principles