Time, Clocks & Ordering
Time is not global in distributed systems — logical clocks give us the only ordering that actually matters: causality.
Overview
Time, Clocks & Ordering
If you have ever debugged a race condition, you have brushed up against one of the deepest problems in distributed systems: what does it mean for one event to happen "before" another when the computers involved have no shared memory and no perfect clock?
In a single-threaded program on one machine, time is simple. The CPU executes instructions in a sequence; the operating-system clock ticks monotonically; "before" is well-defined. But the moment you have two machines communicating over a network, that clarity evaporates. Each machine has its own quartz-crystal oscillator, drifting at its own rate. Network messages take variable time — or get reordered. There is no oracle you can ask "what time is it right now across all machines simultaneously?" — Einstein's special relativity tells us that simultaneity is not absolute, and distributed computing rediscovered this truth the hard way.
This module builds the theory of time in distributed systems from the ground up. We start with physical clocks and their unavoidable imperfections. Then we make a crucial conceptual leap: we stop caring about wall-clock time and start caring about causality. Leslie Lamport's 1978 paper "Time, Clocks, and the Ordering of Events in a Distributed System" — one of the most cited papers in computer science — gave us the happens-before relation and logical clocks, tools that let us reason about ordering without any global clock at all. We then extend to vector clocks, which capture causality exactly. Along the way, we encounter the difference between partial and total order, and see why getting these distinctions right is the foundation of consistent distributed databases, distributed tracing, and conflict-free replicated data types (CRDTs).
By the end of this module you will have the vocabulary and tools to reason precisely about what can be known about the ordering of events in any distributed system, and why certain problems — like deciding which of two concurrent writes happened "first" — are fundamentally unsolvable without additional coordination.
The ordering of events is the atomic unit of consistency in distributed systems. Every database that promises "serializability", every message queue that guarantees "exactly-once delivery in order", and every version-control system that merges concurrent edits is, at its core, solving a problem of event ordering. Get it wrong and you get anomalies: a bank balance that goes negative because two withdrawals were processed in the wrong order, a chat message that appears before the message it was replying to, or a replicated database where two nodes disagree on which write was "last". These are not bugs in the code — they are consequences of ignoring the fundamental physics of distributed time.
Vector clocks underpin systems you use every day. Amazon's Dynamo (the ancestor of DynamoDB and Cassandra) uses vector clocks to detect concurrent writes. Git uses a causal DAG that is structurally identical to the happens-before relation. Google's Spanner uses TrueTime — a physical-clock system with bounded uncertainty — to implement external consistency. Distributed tracing systems like Jaeger and Zipkin use causal timestamps to reconstruct the execution of a request across dozens of microservices. Understanding this material is not academic exercise: it is the prerequisite for understanding almost every advanced topic in distributed systems, from consensus (Paxos/Raft) to CRDTs to distributed transactions.
send: C = C + 1
recv: C = max(C, msg) + 1
a → c ⇢ d → f. Each link forces a strictly larger clock, so A → B implies C(A) < C(B).
e ∥ g — no message or program-order path connects them. Lamport can't order them; equal-ish clocks don't imply causality.
Step the events, or hover any node to light up everything that happened-before it. Numbers inside circles are Lamport timestamps.
Happens-Before in a 3-Process System with Vector Clocks
Three processes exchanging messages. Vector clocks grow via component-wise max on receive plus increment of own component. Events a and b have VC=[1,0,0] and [0,1,0] respectively — neither dominates, so they are concurrent. Event g at P0 has VC=[3,3,4], which is greater than every earlier event shown, meaning g is causally after all of them.
The concepts
01Physical Clocks and Clock Skew
Every computer has a hardware clock: typically a quartz crystal oscillator that vibrates at a nominal frequency (often 32,768 Hz), plus a counter that the operating system reads and calibrates into wall-clock time. The OS exposes this as the system clock (via gettimeofday on Linux, or clock_gettime(CLOCK_REALTIME, ...)), returning the number of seconds since the Unix epoch (Jan 1, 1970 UTC).
The problem: quartz oscillators are not perfect. They drift. A typical server clock drifts by 10–100 ppm (parts per million), meaning it can gain or lose up to 8.64 seconds per day. Over the lifetime of a server between reboots, this adds up to minutes or hours of error.
To counter this, servers run NTP (Network Time Protocol), which synchronizes clocks against a hierarchy of time servers that ultimately reference atomic clocks. NTP can reduce skew to 1–50 milliseconds on a LAN, but over a WAN the skew is often 100 ms or more. NTP can even step the clock backwards when it is running too fast, which violates monotonicity and can cause now() < last_recorded_time — a catastrophic assumption violation for any system that uses timestamps to order events.
Clock skew is the difference in readings between two clocks at a given instant. Clock drift is the rate at which skew accumulates over time. If Process A records timestamp T_A and Process B records timestamp T_B for events that A claims happened before B's event, but T_A > T_B due to skew, we have a false ordering.
Operating systems also expose a monotonic clock (CLOCK_MONOTONIC on Linux, time.monotonic() in Python) that is guaranteed never to go backward — it counts nanoseconds since some arbitrary point (often boot). This is safe for measuring durations on one machine but cannot be compared across machines, since the epoch differs.
Google's Spanner database confronts this head-on with TrueTime: GPS receivers and atomic clocks in every datacenter, combined with a careful error-bound API (TT.now() returns an interval [earliest, latest] rather than a point). Spanner waits out the uncertainty interval before committing a transaction, ensuring that if commit timestamp s is in the interval, no future transaction will be assigned a timestamp less than s. This is a remarkable engineering achievement that costs tens of milliseconds of commit latency in exchange for global external consistency.
import time
# System (wall) clock — NOT safe to compare across machines
t_wall = time.time() # seconds since Unix epoch, can go backward
# Monotonic clock — safe for durations on ONE machine only
t_mono = time.monotonic() # seconds since arbitrary epoch, never decreases
# Perf counter — highest-resolution monotonic clock available
t_perf = time.perf_counter() # best for benchmarking on one machine
print(f"Wall clock: {t_wall:.6f}")
print(f"Monotonic: {t_mono:.6f}")
print(f"Perf: {t_perf:.9f}")
# Danger: comparing timestamps across processes/machines
# If machine A has clock_skew = +200ms vs machine B:
# event on A at t=1000.200, event on B at t=1000.100
# BUT A's event actually happened AFTER B's event!
# Physical timestamps alone cannot establish causal order.
02The Happens-Before Relation
Leslie Lamport's key insight in his 1978 paper was to abandon wall-clock time entirely and define a partial order on events based purely on information flow. He called this the happens-before relation, written → (read: "happened before" or "causally precedes").
Formal definition: the happens-before relation → on a set of events in a distributed system is the smallest relation satisfying:
- Process order: If
aandbare events in the same process andaoccurs beforeb, thena → b. - Message causality: If
ais the sending of a message andbis the receipt of that same message, thena → b. - Transitivity: If
a → bandb → c, thena → c.
These three rules capture causality precisely: a → b means it is possible that a influenced b. If a → b, then any process that has observed b could, in principle, have had its behavior influenced by a.
What if neither a → b nor b → a? Then a and b are concurrent, written a ∥ b. Concurrency here means: neither event could have caused the other. There was no causal path — no chain of messages or process-order steps — from a to b or from b to a.
This gives us a strict partial order on events (irreflexive, asymmetric, transitive) that is not total: some pairs of events are incomparable. This is not a limitation — it is a profound truth about distributed systems. When two writes are concurrent, there is no objective fact about which came first. Any claim of an ordering is an arbitrary choice, not a discovery.
Visualizing this: imagine events as nodes in a directed acyclic graph (DAG). Draw an edge from a to b if a → b directly (by rule 1 or 2). The transitive closure of this DAG is the full happens-before relation. Two nodes with no directed path between them in either direction are concurrent.
# Happens-before as a DAG (conceptual)
# Process P: a -> b -> c
# Process Q: d -> e
# Process R: f -> g -> h
# Messages: b -> e (P sends to Q), d -> g (Q sends to R)
# Edges in DAG:
# a->b, b->c (process order in P)
# d->e (process order in Q)
# f->g, g->h (process order in R)
# b->e (message: P sends to Q)
# d->g (message: Q sends to R)
# Transitive closure gives us:
# a -> b -> c (P's process order)
# a -> e (a->b->e)
# b -> g (b->e->... wait, d->g, not e->g)
# Actually: a->b->e (but e is AFTER d->g path needs checking)
# b->e, d->e (both happen before e), d->g->h
# b->e means e happens after b; b->g? Only if e->g, which is not stated.
# So: b || f, b || g (b and g are CONCURRENT — no causal path!)
def happens_before(dag_edges, a, b):
"""BFS/DFS reachability in the happens-before DAG."""
from collections import deque
visited = set()
queue = deque([a])
while queue:
node = queue.popleft()
if node == b:
return True
if node not in visited:
visited.add(node)
queue.extend(dag_edges.get(node, []))
return False
edges = {'a':['b'], 'b':['c','e'], 'd':['e','g'], 'f':['g'], 'g':['h']}
print(happens_before(edges, 'a', 'c')) # True (a->b->c)
print(happens_before(edges, 'b', 'h')) # True (b->e, d->g->h, but b->e doesn't reach g...)
# b->e, and d->g->h; is e->g? No edge. So b->h is FALSE.
print(happens_before(edges, 'b', 'h')) # False — b and h are concurrent!
03Lamport Timestamps
Now that we have a precise definition of happens-before, we want to assign numbers to events such that if a → b, then timestamp(a) < timestamp(b). This is a clock condition: the clock must be consistent with causality.
Lamport's algorithm is elegantly simple. Each process P_i maintains a counter C_i, initially 0:
- Before executing any event:
C_i = C_i + 1. Assign timestampC_ito the event. - When sending a message: include the current
C_iin the message. - When receiving a message with timestamp
t:C_i = max(C_i, t) + 1.
Rule 3 is the key: when you receive a message, you fast-forward your clock to at least the sender's clock value, then increment. This ensures the receive event has a strictly larger timestamp than the send event.
Clock condition theorem: If a → b, then L(a) < L(b). (Proof: by structural induction on the rules defining →.)
Important caveat — the converse is NOT true: L(a) < L(b) does NOT imply a → b. Two concurrent events a ∥ b might have L(a) < L(b) by coincidence. Lamport timestamps are a necessary but not sufficient condition for happens-before.
What Lamport timestamps ARE good for:
- Breaking ties arbitrarily to achieve a total order: if
L(a) ≠ L(b), sort by timestamp; ifL(a) = L(b), break ties by process ID. This total order is consistent with happens-before (no event appears before a cause) but is an arbitrary extension of the partial order (concurrent events get an arbitrary ordering). - This total order is the basis of Lamport's Mutex algorithm and many distributed algorithms that need a globally consistent ordering without a centralized sequencer.
The key limitation: from L(a) < L(b), you cannot tell whether a → b or a ∥ b. Lamport timestamps tell you what order is consistent with causality, but not what the actual causal structure is.
import threading
class LamportClock:
def __init__(self, pid):
self.pid = pid
self.time = 0
self._lock = threading.Lock()
def tick(self):
"""Increment before a local event. Returns (timestamp, pid) tuple."""
with self._lock:
self.time += 1
return (self.time, self.pid)
def send(self):
"""Call before sending; returns clock value to embed in message."""
return self.tick()
def receive(self, msg_time):
"""Call on receiving a message with timestamp msg_time."""
with self._lock:
self.time = max(self.time, msg_time) + 1
return (self.time, self.pid)
# Simulation: P0 sends to P1
p0 = LamportClock(0)
p1 = LamportClock(1)
e1 = p0.tick() # P0 local event -> (1, 0)
e2_send, t_sent = p0.send(), p0.time # P0 sends msg -> (2, 0), t_sent=2
e3 = p1.tick() # P1 local event -> (1, 1) [concurrent with P0!]
e4_recv = p1.receive(t_sent) # P1 receives -> max(1,2)+1 = (3, 1)
# Now L(send)=2 < L(recv)=3 ✓
# But L(e3)=1 < L(e2_send)=2 does NOT mean e3 happened-before e2!
# e3 and e2 are concurrent events that got an arbitrary ordering.
04Vector Clocks
Lamport timestamps give us a total order consistent with causality, but they lose causal structure: we cannot detect concurrency. Vector clocks, independently developed by Colin Fidge and Friedemann Mattern in 1988, fix this by tracking a vector of counters — one per process — instead of a single counter.
With n processes, each process P_i maintains a vector V_i[1..n], initially all zeros.
Update rules:
- Before a local event at P_i:
V_i[i] = V_i[i] + 1. - When P_i sends a message: increment
V_i[i], then send the current vectorV_iwith the message. - When P_i receives a message with vector
V_msg:V_i[j] = max(V_i[j], V_msg[j])for allj, thenV_i[i] = V_i[i] + 1.
The component V_i[j] records how many events P_i knows have happened at P_j that are causally before the current event at P_i.
Comparing vector clocks: Given vectors V and W:
V = WiffV[i] = W[i]for alliV ≤ WiffV[i] ≤ W[i]for alliV < WiffV ≤ WandV ≠ WV ∥ W(concurrent) iff neitherV < WnorW < V
Theorem (Vector Clock Consistency): For events a and b with vector timestamps VC(a) and VC(b):
a → bif and only ifVC(a) < VC(b)a ∥ bif and only ifVC(a) ∥ VC(b)
This is the crucial difference from Lamport: vector clocks are both necessary AND sufficient for happens-before. You can detect concurrency exactly. The price: O(n) space per timestamp, where n is the number of processes. For large clusters this can be prohibitive (Amazon Dynamo famously ran into this with hundreds of storage nodes and eventually switched to a truncated/bounded variant).
Vector clocks are used in: Amazon Dynamo (conflict detection), Riak (sibling resolution), version control systems (Git's DAG is structurally equivalent), distributed databases for snapshot isolation, and distributed debugging.
class VectorClock:
def __init__(self, pid, n_procs):
self.pid = pid
self.n = n_procs
self.vec = [0] * n_procs
def tick(self):
self.vec[self.pid] += 1
return self.vec[:]
def send(self):
self.vec[self.pid] += 1
return self.vec[:] # send a copy
def receive(self, msg_vec):
for i in range(self.n):
self.vec[i] = max(self.vec[i], msg_vec[i])
self.vec[self.pid] += 1
return self.vec[:]
@staticmethod
def compare(v1, v2):
"""Returns 'before', 'after', 'equal', or 'concurrent'."""
less = any(v1[i] < v2[i] for i in range(len(v1)))
greater = any(v1[i] > v2[i] for i in range(len(v1)))
if not less and not greater: return 'equal'
if less and not greater: return 'before'
if greater and not less: return 'after'
return 'concurrent'
# 3-process example
p0 = VectorClock(0, 3) # P0
p1 = VectorClock(1, 3) # P1
p2 = VectorClock(2, 3) # P2
a = p0.tick() # a: [1,0,0]
b = p1.tick() # b: [0,1,0] — concurrent with a!
c_vec = p0.send() # P0 sends: [2,0,0]
d = p1.receive(c_vec) # P1 receives from P0: max+1 -> [2,2,0]
e = p2.tick() # e: [0,0,1] — concurrent with everything above
print(VectorClock.compare(a, d)) # 'before' — a caused d
print(VectorClock.compare(b, d)) # 'before' — b caused d
print(VectorClock.compare(a, b)) # 'concurrent' — no causal path
print(VectorClock.compare(e, d)) # 'concurrent' — P2 isolated
05Partial Order vs Total Order
These terms come from mathematics but have deep practical implications.
A strict partial order is a relation < on a set S that is:
- Irreflexive:
a < ais false for alla - Asymmetric: if
a < bthenb < ais false - Transitive: if
a < bandb < cthena < c
The happens-before relation → is a strict partial order on the events of a distributed system. It is partial because not all pairs are comparable — concurrent events have no ordering.
A total order adds comparability (also called trichotomy or linearity):
- For all
a ≠ b: eithera < borb < a
Every pair of events can be compared. A total order consistent with a given partial order is called a linear extension of that partial order.
Why does this distinction matter?
-
Databases need total orders for serializable transactions: Serializability requires that concurrent transactions appear as if they executed in some serial (total) order. But which one? Any linear extension of the happens-before order is valid. Distributed databases spend enormous effort agreeing on one.
-
Total orders can be arbitrary: When we extend a partial order to a total order (as Lamport timestamps do), we are making an arbitrary choice about the ordering of concurrent events. This is fine for many algorithms (mutual exclusion, leader election), but if concurrent events are writes to the same key, you have just invented a "winner" with no causal justification.
-
CRDTs exploit partial orders: Conflict-Free Replicated Data Types are designed so that their merge operation is commutative and associative — it doesn't matter what order concurrent updates are applied in. This is the principled way to handle concurrency without a total order.
-
Total order broadcast (atomic broadcast): A fundamental distributed-systems primitive: deliver messages to all processes in the same total order. This is equivalent in power to consensus (Paxos, Raft) — you cannot implement total-order broadcast without solving consensus. This is why ordering is hard in distributed systems: to get a total order you need coordination, and coordination is expensive and fragile.
# Demonstrating partial vs total order over events
# Events: a, b, c, d where a->c, b->c, d is concurrent with all
from functools import cmp_to_key
events = {
'a': [1, 0, 0], # vector clocks
'b': [0, 1, 0],
'c': [1, 1, 1], # received from both a(via P0) and b(via P1)
'd': [0, 0, 1], # P2 local, concurrent with a and b
}
def vc_le(v1, v2):
return all(v1[i] <= v2[i] for i in range(3))
def partial_order_compare(e1, e2):
v1, v2 = events[e1], events[e2]
if vc_le(v1, v2) and v1 != v2: return -1 # e1 before e2
if vc_le(v2, v1) and v1 != v2: return 1 # e2 before e1
return 0 # concurrent — NOT a valid comparator for total sort!
# Lamport-style total order: use sum of vector as proxy (tie-break by name)
def lamport_total_order(e1, e2):
s1 = sum(events[e1])
s2 = sum(events[e2])
if s1 != s2: return s1 - s2
return (e1 > e2) - (e1 < e2) # lexicographic tie-break
total = sorted(events.keys(), key=cmp_to_key(lamport_total_order))
print("Total order:", total) # e.g. ['a','b','d','c'] or similar
# Note: a and b may swap, d may appear before or after them.
# Any ordering of {a,b,d} before c is a valid linear extension.
06Causality Violations and Why They Matter
A causality violation occurs when the observable state of a system contradicts the happens-before relation. The classic example is causal consistency in distributed databases.
Imagine Alice posts on a social network: "I got the job!" (event A). Then Bob sees Alice's post and comments: "Congratulations!" (event B, causally after A). Now Carol refreshes and sees Bob's comment but NOT Alice's post. Carol is confused — she sees an effect without its cause. This is a causality violation, and it can happen easily when different replicas are in different states.
Causal consistency is the guarantee that causally related operations are seen by all processes in causal order. It is weaker than linearizability (which requires a total order consistent with real time) but stronger than eventual consistency (which allows arbitrary orderings). Many practical systems — including MongoDB's causal sessions and YugabyteDB — offer causal consistency as a performance/correctness trade-off.
Read-your-writes is a practical special case: if you write a value and then read, you should see your own write. This is violated in naive round-robin load balancing where your read goes to a replica that hasn't received your write yet.
Vector clocks give us a precise tool to detect and prevent causality violations. When a client operation arrives at a server, include the client's vector clock. The server can check whether it has "seen" all the events the client expects — if not, it must wait (or re-route) before serving the request. Systems like Bayou, COPS (Causal+ Consistency), and Eiger use this technique.
The CAP theorem connection: Causal consistency is achievable in a partition-tolerant, available system (unlike linearizability, which CAP tells us cannot be provided under partition). COPS and similar systems prove this: you can have causal consistency AND availability AND partition tolerance, as long as you accept that some operations (like atomic multi-key writes) require additional coordination.
# Simulating a causality violation in a replicated key-value store
# and how vector clocks prevent it
class CausalKVStore:
def __init__(self, node_id, n_nodes):
self.nid = node_id
self.store = {} # key -> (value, vector_clock)
self.vc = [0] * n_nodes
def write(self, key, value):
self.vc[self.nid] += 1
self.store[key] = (value, self.vc[:])
return self.vc[:] # return clock to client
def read(self, key, client_vc):
"""Only serve read if we have seen at least client_vc."""
if key not in self.store:
return None, None
_, stored_vc = self.store[key]
# Check: is client_vc <= stored_vc? (have we seen all client deps?)
deps_satisfied = all(
self.vc[i] >= client_vc[i] for i in range(len(client_vc))
)
if deps_satisfied:
return self.store[key]
else:
return 'WAIT', None # must stall until deps are replicated
# Without causal tracking, Bob's comment could arrive at a replica
# before Alice's post — causing Carol to see the comment sans post.
# With vector clocks, the replica sees the client's dependency vector
# and refuses to serve the comment until Alice's post is also visible.
07Consistent Global Snapshots (Chandy-Lamport)
A global state of a distributed system is the collection of local states of all processes plus the states of all communication channels (messages in transit). A consistent cut is a global state where, for every message received in the cut, the corresponding send event is also in the cut. In other words, you never see an effect without its cause.
The Chandy-Lamport snapshot algorithm (1985) computes a consistent global snapshot of a distributed system without stopping it. It is used for:
- Checkpointing (save state to recover from failure)
- Detecting global properties like deadlock or termination
- Debugging (what was the state of the whole system when this bug appeared?)
Algorithm (assumes reliable FIFO channels between every pair of processes):
- Any process
P_ican initiate by recording its own local state and sending a special marker message on all outgoing channels. - When a process
P_jreceives a marker on channelC_ijfor the first time: a. It records its local state. b. It records the channel stateC_ijas empty (the marker is the cut point). c. It sends a marker on all its outgoing channels. - After initiating/receiving the first marker, for any other channel
C_kjwhereP_jhas NOT yet received a marker:P_jrecords all messages received onC_kjafter the first marker (these are in-transit messages that are part of the snapshot). - The algorithm terminates when all processes have received markers on all their incoming channels.
Why is this a consistent cut? The marker partitions the event stream at each process into "before snapshot" and "after snapshot". Any message sent before the marker is in the snapshot; messages sent after are not. By recording channel states (messages received after the sender marked but before the receiver marked), we capture in-flight messages that belong to the pre-snapshot world. This guarantees the cut is consistent — no message appears received without its send.
# Chandy-Lamport snapshot — conceptual single-process simulation
# Real implementation would span multiple processes/machines.
MARKER = '__MARKER__'
class Process:
def __init__(self, pid):
self.pid = pid
self.state = {} # application state (e.g. account balance)
self.recording = {} # channel -> list of in-transit msgs
self.snapshot_state = None
self.snapshot_channels = {}
self.initiated = False
def initiate_snapshot(self, outgoing_channels):
"""Step 1: initiating process records own state, sends markers."""
self.snapshot_state = dict(self.state)
self.initiated = True
for ch in outgoing_channels:
ch.send(MARKER)
# Start recording all incoming channels
for ch in self.incoming_channels:
self.recording[ch] = []
def on_receive(self, msg, from_channel, outgoing_channels):
if msg == MARKER:
if not self.initiated:
# First marker: record state, send markers, stop recording this channel
self.snapshot_state = dict(self.state)
self.initiated = True
for ch in outgoing_channels:
ch.send(MARKER)
for ch in self.incoming_channels:
if ch != from_channel:
self.recording[ch] = []
# Finalize channel state for from_channel
self.snapshot_channels[from_channel] = self.recording.pop(from_channel, [])
else:
# Normal message: if recording this channel, capture it
if from_channel in self.recording:
self.recording[from_channel].append(msg)
self._process(msg)
def _process(self, msg):
pass # application logic
08Version Vectors and Conflict Detection
Version vectors (also sometimes loosely called vector clocks in the storage community, though they are subtly different) are the application of vector-clock ideas to replica versioning in distributed storage systems.
In a system where each key can be written by multiple replicas (like Dynamo or Riak), each value is tagged with a version vector: a mapping from replica ID to counter, recording how many writes from each replica are reflected in this version.
Difference from vector clocks: In vector clocks, the clock advances at the process that does the operation. In version vectors, the counter advances at the replica that accepted the write. The semantics are different: version vectors track which updates have been incorporated into a value, while vector clocks track causal relationships between all events.
Conflict detection with version vectors:
- If version
V1 < V2(component-wise), then V2 subsumes V1 — V2 is causally newer. Keep V2. - If
V1 ∥ V2(neither dominates), the two values were written concurrently — this is a conflict that the application must resolve (e.g., "last writer wins", "merge", "show both to user").
Amazon Dynamo's original design used version vectors and returned conflicting sibling values to the client, which resolved them. Riak still does this. DynamoDB switched to "last writer wins" using physical timestamps (simple but can lose data). Cassandra uses a hybrid: last-writer-wins with wall-clock timestamp, relying on NTP (imperfect but practical).
The danger of "last writer wins" (LWW): If two concurrent writes happen and you pick the one with the higher physical timestamp as the winner, you silently discard one write. This is data loss. The only way to avoid it without coordination is to detect concurrency using vector clocks and handle it at the application layer.
Dotted version vectors: An advanced variant that solves the problem of distinguishing "this replica has seen and deleted a key" from "this replica hasn't heard about the key yet" — a distinction that plain version vectors cannot make. Used in Riak 2.0.
# Version vector conflict detection for a distributed key-value store
def vv_dominates(v1, v2):
"""Returns True if v1 causally dominates (is newer than or equal to) v2."""
all_keys = set(v1) | set(v2)
return all(v1.get(k, 0) >= v2.get(k, 0) for k in all_keys)
def merge_values(val1, vv1, val2, vv2):
"""
Given two versions of a key, return the resolved value(s).
Returns a single value if one dominates; list of siblings if concurrent.
"""
d12 = vv_dominates(vv1, vv2) # is v1 >= v2 everywhere?
d21 = vv_dominates(vv2, vv1) # is v2 >= v1 everywhere?
if d12 and not d21:
return [(val1, vv1)] # v1 is strictly newer
elif d21 and not d12:
return [(val2, vv2)] # v2 is strictly newer
elif d12 and d21:
return [(val1, vv1)] # equal — same write, keep either
else:
return [(val1, vv1), (val2, vv2)] # CONFLICT — concurrent writes!
# Replica A wrote 'alice' at counter A=1
vv_a = {'A': 1}
# Replica B wrote 'bob' at counter B=1 CONCURRENTLY (neither knew about the other)
vv_b = {'B': 1}
result = merge_values('alice', vv_a, 'bob', vv_b)
print(result) # Two siblings: conflict! Application must resolve.
# After resolution: client merges and writes back with merged vv
vv_merged = {'A': 1, 'B': 1} # "I've seen both A=1 and B=1"
result2 = merge_values('alice+bob', vv_merged, 'bob', vv_b)
print(result2) # Single value: merged dominates
Key insights
- ✦Physical clocks drift and can go backward (after NTP sync), making them unsafe for ordering events across machines — never use wall-clock timestamps to establish causal order in distributed systems.
- ✦The happens-before relation (→) captures causality exactly: a → b means information could have flowed from a to b. Events with no causal path are concurrent (∥) — neither happened 'first' in any objective sense.
- ✦Lamport timestamps satisfy the clock condition (a → b implies L(a) < L(b)) but not its converse — you cannot detect concurrency from Lamport timestamps alone.
- ✦Vector clocks satisfy the strong clock condition: a → b IF AND ONLY IF VC(a) < VC(b). They detect concurrency exactly, at the cost of O(n) space per timestamp.
- ✦A total order on events is an arbitrary choice when extending a partial order — it imposes an ordering on concurrent events that has no causal basis. Distributed consensus algorithms (Paxos, Raft) are the cost you pay to get a total order.
- ✦Causality violations (seeing an effect without its cause) are observable and harmful — vector clocks enable systems to stall reads until causal dependencies are satisfied.
- ✦The Chandy-Lamport snapshot algorithm captures a consistent global state without pausing the system, by using marker messages to define a cut that respects causality.
- ✦Version vectors and vector clocks are structurally similar but semantically distinct — version vectors track which updates a value incorporates; vector clocks track causal relationships between events.
Common pitfalls
- ⚠Confusing the clock condition (Lamport) with the strong clock condition (vector clocks): L(a) < L(b) does NOT mean a → b. Students often assume Lamport timestamps detect concurrency — they do not.
- ⚠Using wall-clock timestamps (time.time() in Python, System.currentTimeMillis() in Java) to order distributed events — NTP skew means these can be wrong by hundreds of milliseconds, and the clock can go backward.
- ⚠Confusing concurrent (no causal relationship) with simultaneous (happening at the same physical instant) — these are completely different concepts. Concurrent events may be physically far apart in time.
- ⚠Forgetting to increment the vector clock component for the *receiving* process on message receipt — the rule is max-merge first, then increment your own component. Doing only the max-merge without incrementing is a common bug.
- ⚠Assuming vector clocks scale to large clusters — with 1000 nodes, every message carries a 1000-element vector. Systems like Dynamo explicitly had to deal with unbounded vector clock growth and switched to bounded/pruned variants.
- ⚠Conflating 'last writer wins' with correctness — LWW silently discards concurrent writes. It is a pragmatic choice, not a correct resolution of concurrency. Use it only when data loss on concurrent writes is acceptable.
- ⚠Misunderstanding the Chandy-Lamport algorithm's requirement for FIFO channels — if channels can reorder messages, the algorithm breaks. Real systems using this algorithm (or variants) must enforce channel ordering or account for it explicitly.
Practice
- 01medium
**Happens-Before DAG**: Three processes P0, P1, P2 execute the following events in process-local order. P0: a, b, c. P1: d, e, f. P2: g, h, i. Messages are sent: b→e (P0 sends to P1), d→g (P1 sends to P2), h→c (P2 sends to P0). (a) Draw the happens-before DAG. (b) List all pairs of concurrent events. (c) Assign Lamport timestamps. (d) Assign vector clocks and verify that concurrent pairs have incomparable vector clocks.
- 02hard
**Implement Vector Clocks with Causal Delivery**: Write a `CausalBroadcast` class that delivers messages to the application layer only when all causally preceding messages have already been delivered. Internally, buffer out-of-order messages. Test it by creating a scenario where messages arrive out of causal order and verifying that delivery is properly delayed.
- 03hard
**Prove the Strong Clock Condition**: Prove formally that for the vector clock algorithm as defined, `a → b` if and only if `VC(a) < VC(b)`. Structure your proof as two directions: (1) `a → b` implies `VC(a) < VC(b)` by induction on the definition of →; (2) `VC(a) < VC(b)` implies `a → b` by contrapositive (if a ∥ b then VC(a) ∥ VC(b)).
- 04medium
**Chandy-Lamport on Paper**: Simulate the Chandy-Lamport snapshot algorithm on a 3-process system with bidirectional FIFO channels between all pairs. Process P0 initiates the snapshot after sending a message to P1. Show the state recorded at each process and each channel. Verify that your cut is consistent by checking that no message has its receive in the cut but its send outside.
- 05easy
**Last Writer Wins vs. Vector Clocks**: Implement a tiny replicated key-value store with two replicas that can receive writes independently (simulating a network partition). First implement LWW (last writer wins by physical timestamp). Then implement proper conflict detection with version vectors. Create a test scenario where LWW loses data (two concurrent writes) and show that the version vector approach detects the conflict instead of silently dropping a write.
Resources
The original paper — mandatory reading. Remarkably readable for a seminal paper. 12 pages.
Chapter 8 (Trouble with Distributed Systems) and Chapter 9 (Consistency and Consensus). The best practical treatment of these ideas in a systems context.
Maarten van Steen and Andrew Tanenbaum. Free PDF available from the authors. Chapters on naming and synchronization cover logical clocks thoroughly.
MIT's flagship distributed systems course. Lecture notes, papers, and lab assignments (Raft implementation). The reading list alone is a complete education.
Carlos Baquero and Nuno Preguica, ACM Queue 2016. Excellent short article on the intuition behind vector clocks and version vectors.
The paper that made vector clocks famous in industry. Describes real-world use of version vectors for conflict detection at Amazon's scale.
Describes TrueTime — the physical clock system that lets Google use bounded uncertainty to achieve external consistency. Fascinating contrast to purely logical approaches.
Solid overview with good references. Use as a quick reference, not a primary learning source.
Checkpoints — you can now…
- ✓You can explain why physical clocks are insufficient for ordering distributed events, and name two situations where NTP can introduce incorrect orderings.
- ✓You can state Lamport's three rules for the happens-before relation from memory, identify concurrent events in a space-time diagram, and explain why concurrency is not a bug but a fundamental property.
- ✓You can trace through the Lamport timestamp algorithm on a multi-process example and verify the clock condition — while explaining why the converse does not hold.
- ✓You can trace through the vector clock algorithm, compare two vector timestamps, and correctly identify whether one event happened-before the other or they are concurrent.
- ✓You can explain the difference between a partial order and a total order, and describe what choices are being made (and what is being lost) when a distributed system extends the partial causal order to a total order.
- ✓You can describe at least one real system (Dynamo, Riak, Git, Cassandra) and explain which clock/ordering mechanism it uses and why.
- ✓You can sketch the Chandy-Lamport snapshot algorithm and verify that a given cut is consistent.