Two servers each timestamp an event. Server A says 10:00:00.003, server B says 10:00:00.001. Did B’s event happen first? You genuinely cannot know: NTP keeps commodity clocks within single-digit milliseconds when healthy (and lies much harder when not — VM migrations, leap-second smears, a dying oscillator), while your systems do meaningful work in microseconds. Every “sort by timestamp across machines” design contains this bug. Lamport’s 1978 paper — arguably the founding document of distributed systems — dissolved the problem by replacing the question: not when did events happen, but could one have caused the other?
Happened-Before: Causality as the Only Real Order
Define a → b (a happened-before b) if: they’re in one process and a
came first; or a is a message send and b its receive; or transitivity
chains them. Crucially, some pairs have neither a → b nor
b → a — they are concurrent, and no amount of clock precision
changes that; it’s a statement about information flow, not physics
trivia. Everything in this field is machinery for tracking →.
Lamport clocks are the minimal version: a counter per process —
increment on each local event, stamp messages, and on receive set
counter = max(local, received) + 1. The guarantee is one-directional:
a → b ⇒ L(a) < L(b). The converse fails — smaller timestamp does
not mean earlier/causal — and that asymmetry is exactly what people
forget. What Lamport clocks are good for: a total order consistent
with causality (order by (L, process-id) tiebreak), which is enough
for distributed mutual exclusion, log ordering in consensus protocols
(Raft’s terms are Lamport-flavored), and “newest wins” schemes where
you accept arbitrary-but-consistent tiebreaks.
Vector clocks buy the converse. Each process keeps a vector of counters, one slot per participant; merge element-wise-max on receive:
V(a) = [2,1,0] V(b) = [3,1,0] → V(a) < V(b): a → b (causal)
V(a) = [2,1,0] V(c) = [1,2,0] → incomparable: CONCURRENTNow V(a) < V(b) ⟺ a → b, and incomparability detects concurrency
— which is the operationally valuable power: Dynamo-lineage stores
used vector clocks to distinguish “this write supersedes that one”
(discard the old) from “these writes are concurrent” (keep both as
siblings, make the reader reconcile) — the mechanism behind Riak’s
sibling values, and the thing you silently lose with
last-write-wins-by-wall-clock, which resolves every conflict by
destroying one side (Cassandra’s LWW is exactly this trade, chosen
with eyes open). The cost is the vector: O(participants) per stamp,
awkward with churning membership (dotted version vectors and
client-ID pruning are the patch literature), which is why real
systems spend it only per-key, not per-message.
The Compromise That Won: Hybrid Logical Clocks
Pure logical time answers causality but can’t answer “give me
yesterday’s state” — humans and SQL live in wall time. HLC (2014)
fuses them: a timestamp that stays within bounded distance of physical
NTP time but ticks logically (max+increment) on message exchange, so
it never violates causality even when clocks skew. That’s what
CockroachDB runs on, and MongoDB’s cluster times are the same idea —
you get →-consistent ordering and meaningful timestamps in one
64-bit-ish value. The maximalist alternative is Spanner’s TrueTime:
GPS+atomic clocks tighten uncertainty to a few ms, and transactions
wait out the uncertainty interval before committing — buying
external consistency with specialized hardware plus deliberately
spent latency. Both designs concede the same premise: wall clocks
alone are unfit to order events; the question is only how much
machinery you deploy around them.
The practical residue for ordinary backends: never merge/sort cross-machine events by raw wall timestamps when correctness matters; use per-entity version counters (a one-slot vector clock — usually all you need); treat “concurrent” as a real state your data model must represent or explicitly LWW away; and reserve wall time for what it’s for — humans, TTLs, and metrics.
Takeaways
- Clock skew vs event rate makes wall-time ordering fiction; happened-before (information flow) is the only order that exists.
- Lamport clocks: causality implies smaller timestamp, never the reverse — good for consistent total orders, blind to concurrency.
- Vector clocks make concurrency detectable — the difference between merging conflicts and silently deleting one side (LWW).
- HLC = causal correctness + approximate wall time; TrueTime = pay hardware and latency for bounded uncertainty. Same admission, different budgets.
- In practice: per-entity versions, explicit conflict states, wall time for humans only.