Time, Clocks, and Ordering

The most dangerous line of code in a distributed system is the one that trusts a timestamp. Physical clocks on different machines disagree, drift, and jump backward — so "which event happened first?" cannot be answered by comparing wall-clock times. Logical clocks answer it instead, by tracking causality rather than time.

Almost everything in a distributed system eventually needs to order events: which write is newer, which transaction came first, who acquired the lock. On a single machine you’d just check the clock. Across machines, that instinct is a trap, because there is no single clock — each node has its own, and they don’t agree. This post explains why physical time fails as an ordering mechanism and how logical clocks — Lamport timestamps and vector clocks — give you the ordering you actually need.

Why you can’t trust the wall clock

Every machine has a time-of-day clock synchronized (imperfectly) over the network via NTP. Three properties make it unsafe for ordering events across machines:

The practical consequence is brutal: “last write wins,” implemented with wall-clock timestamps, can silently discard the actually-newer write because the machine that made it had a slightly slow clock. Ordering by physical time is ordering by an unreliable, non-monotonic, per-node approximation — and it produces data-loss bugs that are nearly impossible to reproduce.

A note on the exception: some systems (famously Google’s Spanner) use tightly bounded clocks (TrueTime) with GPS and atomic sources, treating time as an interval with known uncertainty and waiting out that uncertainty to order events safely. That works, but it requires special hardware and deliberate waiting — it’s proof that raw wall-clock ordering is unsafe unless you engineer the uncertainty away.

What we actually need: happens-before

The insight that unlocks distributed ordering is that you rarely need to know the real time an event occurred — you need to know whether one event could have caused another. Lamport formalized this as the happens-before relation (written →):

If A → B, then A could have influenced B, so any correct ordering must put A before B. If neither A → B nor B → A, the events are concurrent — they have no causal relationship, and no “true” order between them exists or is needed. Happens-before captures causality, which is the thing ordering actually cares about, and it needs no synchronized clock — only the messages the nodes already exchange.

Lamport clocks: a cheap logical order

A Lamport clock is a single integer counter per node that implements happens-before with minimal machinery:

  1. Each node increments its counter before every local event.
  2. When a node sends a message, it includes its current counter value.
  3. When a node receives a message, it sets its counter to max(local, received) + 1.
Node A:  a1(1) ── send(msg, ts=2) ─────────────┐
                                                 ▼
Node B:  b1(1) ────────────────── recv → max(1,2)+1 = 3

The guarantee: if A → B, then timestamp(A) < timestamp(B). This gives you a consistent total order (break ties by node ID) that never contradicts causality — a slow node can’t make a caused event look earlier than its cause. Lamport clocks are cheap (one integer, piggybacked on messages) and enough whenever you need an order consistent with causality — dedup, log merging, tie-breaking.

Their limitation: the implication only goes one way. timestamp(A) < timestamp(B) does not tell you whether A → B or the two are concurrent. A Lamport clock can order events, but it can’t detect conflicts (concurrent updates), and detecting conflicts is exactly what many replicated systems need.

Vector clocks: detecting concurrency

A vector clock carries more information: each node keeps a vector of counters, one entry per node, representing its latest knowledge of every node’s progress.

  1. Each node increments its own entry before a local event.
  2. Messages carry the whole vector.
  3. On receipt, a node takes the element-wise max of its vector and the received one, then increments its own entry.

Now comparison is richer. For two vectors V(A) and V(B):

This is the piece Lamport clocks can’t provide: vector clocks detect concurrent updates. When two clients update the same key on different replicas during a partition, vector clocks reveal that the versions are concurrent rather than one superseding the other — so the system can preserve both and resolve the conflict (merge, or hand it to the application) instead of silently dropping one. The cost is size: the vector grows with the number of nodes, so systems prune or bound it. The trade is information for space — pay it when you need to detect conflicts, not just order events.

Choosing a clock

The decision follows directly from what you need to know:

The unifying idea: distributed ordering is about causality, tracked through messages, not about time read from a clock. Get that straight and a whole category of “impossible” timestamp bugs disappears. Next we apply this to replication, where concurrent writes — and the clocks that detect them — become concrete.

Key takeaways

Further reading

Sources & References