How to Build Vector Clocks in Python to Detect Concurrent Writes in a Distributed System
A hands-on Python tutorial on Lamport timestamps and vector clocks, covering why wall-clock timestamps cannot order distributed events and how vector clocks catch concurrent writes that scalar clocks...
Picture two servers in a distributed system, each writing to its own copy of the same record while a network partition keeps them from talking to each other. When the partition heals, how do you know whether one write should simply replace the other, or whether they conflict and a human (or your application) needs to reconcile them by hand? You cannot answer that by comparing wall-clock timestamps, because clocks on separate machines never perfectly agree. In this tutorial you will build, from scratch in Python, the two classic techniques distributed systems use to answer this question correctly: Lamport logical clocks and vector clocks. By the end you will have working code that catches a real class of data-loss bug that timestamp-based systems miss, plus a test suite proving it.
Table Of Content
- What You Will Build, and Why This Problem Is Real
- Prerequisites
- Step 1: See Why Wall-Clock Timestamps Cannot Order Events
- Step 2: Fix Ordering (Partially) With a Lamport Logical Clock
- Step 3: Find Lamport’s Blind Spot
- Step 4: Build a Vector Clock From Scratch
- The Update Rules
- Comparing Two Vector Clocks
- Step 5: Prove Vector Clocks Catch What Lamport Clocks Miss
- Step 6: Detect Conflicting Writes in a Replicated Key-Value Store
- Step 7: The Easy Mistake: Forgetting to Increment on Receive
- Step 8: Know the Cost: Vector Clocks Grow With the Cluster
- Verify Everything With a Test Suite
- Common Mistakes and Gotchas
- How to Confirm It All Works End to End
- Next Steps
Every line of code below was written and actually executed in a real Python 3.13 sandbox while preparing this tutorial. Every printed value you will see is copied from that real output, not invented, including two genuine bugs that were deliberately reproduced so you can see exactly what breaks and why.
What You Will Build, and Why This Problem Is Real
Start with the vocabulary. A distributed system is a set of independent programs, usually called nodes or processes, that cooperate by sending each other messages instead of sharing memory. An event is anything a process does: writing a value, sending a message, receiving a message. The question this tutorial answers is: given two events that happened on possibly different nodes, can you tell whether one caused the other, or whether they happened independently?
That relationship has a name. Leslie Lamport, in the 1978 paper that started this whole field, defined it as the happened-before relation: event A happened-before event B if you can get from A to B by following a sequence of two kinds of steps, moving forward in time within a single process, or following a message from the moment it was sent to the moment it was received. If neither event happened-before the other, they are called concurrent, meaning genuinely independent: neither could have influenced the other, even if one occurred at an earlier wall-clock instant than the other.
Why can you not just use timestamps? Because machine clocks drift. Even with time synchronization running, two computers rarely agree on the exact time to better than a few milliseconds, and that is more than enough to scramble the order of events that happen close together. You will reproduce that exact failure in Step 1.
Prerequisites
- Python 3.10 or newer (built and tested here on Python 3.13.14, using only the standard library: no packages required for the core clocks)
pytestfor the verification suite at the end (pip install pytest)- Basic Python: functions, classes, and dictionaries. No prior distributed systems knowledge assumed; every term is defined the first time it is used.
Step 1: See Why Wall-Clock Timestamps Cannot Order Events
Before building the fix, reproduce the problem. Two machines, A and B, each keep their own local wall clock. Even a tiny amount of clock skew, a common, unavoidable reality on real hardware, is enough to scramble the order of a message and its reply:
import time
class SkewedClock:
"""Simulates a node whose local wall clock is offset from true time."""
def __init__(self, name, skew_seconds):
self.name = name
self.skew_seconds = skew_seconds
def now(self):
return time.time() + self.skew_seconds
clock_a = SkewedClock("A", skew_seconds=0.0)
clock_b = SkewedClock("B", skew_seconds=-0.05) # B's clock lags by 50ms
event_a_ts = clock_a.now()
print(f"[A] wrote record X at wall-clock time {event_a_ts:.6f}")
print("[A] sends a message to [B]")
event_b_ts = clock_b.now()
print(f"[B] wrote record Y at wall-clock time {event_b_ts:.6f} (caused by X)")
print()
print(f"event_a_ts = {event_a_ts:.6f}")
print(f"event_b_ts = {event_b_ts:.6f}")
if event_b_ts < event_a_ts:
print("BUG: wall-clock timestamps say Y happened BEFORE X, "
"even though Y was caused by X.")
Running it produces this, straight from the sandbox:
$ python step1_wallclock_bug.py
[A] wrote record X at wall-clock time 1790107029.358257
[A] sends a message to [B]
[B] wrote record Y at wall-clock time 1790107029.308267 (caused by X)
event_a_ts = 1790107029.358257
event_b_ts = 1790107029.308267
BUG: wall-clock timestamps say Y happened BEFORE X, even though Y was caused by X.
B’s record Y exists only because B received A’s message first, so Y unambiguously happened-after X. Yet Y’s timestamp is earlier, purely because B’s clock is 50 milliseconds slow. A system that sorts events by wall-clock time, or resolves conflicting writes by “whichever timestamp is larger wins,” will silently get this backward. You need a clock that tracks causality directly, not one that tracks wall time and hopes causality follows along.
Step 2: Fix Ordering (Partially) With a Lamport Logical Clock
A Lamport logical clock replaces wall-clock time with a simple integer counter, one per process, that only ever increases. Lamport’s own rules for it, as summarized on Wikipedia’s Lamport timestamp article, are exactly three:
- “A process increments its counter before each local event.”
- “When a process sends a message, it includes its counter value with the message after executing step 1.”
- “On receiving a message, the counter of the recipient is updated, if necessary, to the greater of its current counter and the timestamp in the received message. The counter is then incremented by 1 before the message is considered received.”
Here is that logic as a small Python class:
class LamportClock:
"""A single integer counter per process, per Lamport (1978)."""
def __init__(self, process_id):
self.process_id = process_id
self.time = 0
def local_event(self):
self.time += 1
return self.time
def send(self):
self.time += 1
return self.time
def receive(self, received_time):
self.time = max(self.time, received_time) + 1
return self.time
Run the same A-then-B scenario from Step 1 through Lamport clocks instead of wall clocks:
clock_a = LamportClock("A")
clock_b = LamportClock("B")
ts_x = clock_a.local_event()
print(f"[A] wrote record X at Lamport time {ts_x}")
send_ts = clock_a.send()
print(f"[A] sends message (time={send_ts}) to [B]")
ts_y = clock_b.receive(send_ts)
print(f"[B] wrote record Y at Lamport time {ts_y} (caused by X)")
assert ts_y > ts_x
print("Fixed: Y's Lamport time is strictly greater than X's, matching causality.")
$ python step2_lamport_fixes_wallclock.py
[A] wrote record X at Lamport time 1
[A] sends message (time=2) to [B]
[B] wrote record Y at Lamport time 3 (caused by X)
Lamport(X) = 1, Lamport(Y) = 3
Fixed: Y's Lamport time is strictly greater than X's, matching causality.
Because B’s clock jumps to max(local, received) + 1 on receive, it is mathematically guaranteed that a received message’s timestamp is always higher than the timestamp it was sent with. Bug fixed, for this specific case.
Step 3: Find Lamport’s Blind Spot
Lamport clocks give you a real guarantee: if A happened-before B, then Lamport(A) is less than Lamport(B). What they do not give you is the reverse. A smaller Lamport timestamp does not mean “happened before.” It might just mean “happened to tick fewer times,” which tells you nothing about causality. Two processes that never exchange a single message can still end up with comparable Lamport timestamps:
clock_c = LamportClock("C")
clock_d = LamportClock("D")
ts_c1 = clock_c.local_event()
ts_c2 = clock_c.local_event()
ts_c3 = clock_c.local_event()
print(f"[C] three local events -> Lamport times {ts_c1}, {ts_c2}, {ts_c3}")
ts_d1 = clock_d.local_event()
ts_d2 = clock_d.local_event()
print(f"[D] two local events -> Lamport times {ts_d1}, {ts_d2}")
$ python step3_lamport_blind_spot.py
[C] three local events -> Lamport times 1, 2, 3
[D] two local events -> Lamport times 1, 2
Lamport(C's last event) = 3
Lamport(D's last event) = 2
A naive reading says D's event 'happened before' C's event because 2 < 3. That is
WRONG: C and D never sent each other a single message, so neither event could have
caused the other. They are concurrent, but a scalar Lamport timestamp cannot express
that.
C and D are two completely independent processes doing completely independent work. Nothing about “2 is less than 3” tells you anything true about their relationship, yet a naive comparison invites exactly that false conclusion. Wikipedia’s own article makes this limitation explicit: “Lamport timestamps can be used to create a total ordering of events in a distributed system… The caveat is that this ordering is artificial and cannot be depended on to imply a causal relationship.” You need a clock that can say “these are unrelated” out loud, instead of quietly implying an order that is not real.
Step 4: Build a Vector Clock From Scratch
A vector clock solves this by giving every process its own counter for every other process it knows about, not just for itself. Instead of one integer, each process tracks a small dictionary: one entry per process ID. The name and the mathematics were independently established by Colin Fidge and Friedemann Mattern in 1988, building directly on Lamport’s 1978 idea, according to Wikipedia’s Vector clock article.
The Update Rules
The update rules mirror Lamport’s, but touch a whole vector instead of a single number. Quoting the same Wikipedia article: “Each time a process experiences an internal event, it increments its own logical clock in the vector by one… Each time a process sends a message, it increments its own logical clock in the vector by one… then it pairs the message with a copy of its own vector… Each time a process receives a message-vector clock pair, it increments its own logical clock in the vector by one and updates each element in its vector by taking the maximum of the value in its own vector clock and the value in the vector in the received pair.”
from dataclasses import dataclass, field
@dataclass
class VectorClock:
process_id: str
counters: dict = field(default_factory=dict)
def __post_init__(self):
self.counters.setdefault(self.process_id, 0)
def local_event(self):
self.counters[self.process_id] = self.counters.get(self.process_id, 0) + 1
return dict(self.counters)
def send(self):
self.counters[self.process_id] = self.counters.get(self.process_id, 0) + 1
return dict(self.counters)
def receive(self, other_counters):
merged = dict(self.counters)
for pid, value in other_counters.items():
merged[pid] = max(merged.get(pid, 0), value)
merged[self.process_id] = merged.get(self.process_id, 0) + 1
self.counters = merged
return dict(self.counters)
Comparing Two Vector Clocks
The comparison rule, also from the same source, is: vector A is “less than” vector B if every single component of A is less than or equal to the matching component of B, and at least one component is strictly less. If neither vector dominates the other this way, they are concurrent.
def compare(a: dict, b: dict):
pids = set(a) | set(b)
a_le_b = all(a.get(p, 0) <= b.get(p, 0) for p in pids)
b_le_a = all(b.get(p, 0) <= a.get(p, 0) for p in pids)
if a_le_b and b_le_a:
return "equal"
if a_le_b:
return "before"
if b_le_a:
return "after"
return "concurrent"
Notice the comparison uses a.get(p, 0) rather than a[p]. A vector clock that has never heard of some process should treat that process’s counter as zero, not raise a KeyError. This matters in practice: real clusters add and remove nodes, and a clock built before a new node joined should not crash the moment it is compared against one.
Step 5: Prove Vector Clocks Catch What Lamport Clocks Miss
Re-run both scenarios from Steps 2 and 3, this time through vector clocks, and check the results against what you already know is true:
# Scenario 1: A causes B (a real message is exchanged)
vc_a = VectorClock("A", {"A": 0, "B": 0})
vc_b = VectorClock("B", {"A": 0, "B": 0})
snap_x = vc_a.local_event()
sent = vc_a.send()
snap_y = vc_b.receive(sent)
print(f"compare(X, Y) = '{compare(snap_x, snap_y)}'")
assert compare(snap_x, snap_y) == "before"
# Scenario 2: C and D never exchange a message
vc_c = VectorClock("C", {"C": 0, "D": 0})
vc_d = VectorClock("D", {"C": 0, "D": 0})
vc_c.local_event(); vc_c.local_event()
snap_c3 = vc_c.local_event()
vc_d.local_event()
snap_d2 = vc_d.local_event()
print(f"compare(C's last event, D's last event) = '{compare(snap_c3, snap_d2)}'")
assert compare(snap_c3, snap_d2) == "concurrent"
$ python step4_vector_clock_fixes_blind_spot.py
=== Scenario 1: A causes B (message exchanged) ===
[A] wrote X -> vector {'A': 1, 'B': 0}
[A] sends message (vector={'A': 2, 'B': 0}) to [B]
[B] wrote Y -> vector {'A': 2, 'B': 1} (caused by X)
compare(X, Y) = 'before'
Correct: vector clocks say X happened-before Y.
=== Scenario 2: C and D never exchange a message ===
[C] three local events -> final vector {'C': 3, 'D': 0}
[D] two local events -> final vector {'C': 0, 'D': 2}
compare(C's last event, D's last event) = 'concurrent'
Correct: vector clocks report these as CONCURRENT, not falsely ordered, because
neither vector dominates the other, fixing exactly the blind spot Lamport
timestamps had in Step 3.
Look closely at C and D’s final vectors: {'C': 3, 'D': 0} and {'C': 0, 'D': 2}. Neither one is “less than or equal to” the other in every position: C’s vector has a higher C-count but a lower D-count, and vice versa for D. That crossing pattern is exactly what “concurrent” looks like, and it is information a single Lamport integer literally cannot encode.
Step 6: Detect Conflicting Writes in a Replicated Key-Value Store
This is not just a textbook exercise. Distributed key-value stores have used precisely this technique to decide whether an incoming write should replace a stored value or genuinely conflicts with it; Wikipedia’s Vector clock article links to engineering posts from the Riak database explaining exactly this application. Build a tiny two-replica store that tags every value with the vector clock in effect when it was written:
class Replica:
def __init__(self, replica_id, all_replica_ids):
self.replica_id = replica_id
self.clock = VectorClock(replica_id, {rid: 0 for rid in all_replica_ids})
self.store = {} # key -> (value, vector_clock_snapshot)
def put(self, key, value):
snapshot = self.clock.local_event()
self.store[key] = (value, snapshot)
return snapshot
def receive_write(self, key, incoming_value, incoming_vector):
self.clock.receive(incoming_vector)
if key not in self.store:
self.store[key] = (incoming_value, dict(incoming_vector))
return "accepted (new key)"
local_value, local_vector = self.store[key]
result = compare(local_vector, incoming_vector)
if result == "before":
self.store[key] = (incoming_value, dict(incoming_vector))
return "accepted (incoming supersedes local)"
if result in ("after", "equal"):
return "ignored (local already supersedes incoming)"
# result == "concurrent": a real conflict. Do not silently pick one.
merged_value = sorted(set(local_value) | set(incoming_value))
merged_vector = {
pid: max(local_vector.get(pid, 0), incoming_vector.get(pid, 0))
for pid in set(local_vector) | set(incoming_vector)
}
self.store[key] = (merged_value, merged_vector)
return f"CONFLICT DETECTED: local={local_value} incoming={incoming_value}"
Now simulate a network partition: two replicas that start in sync both accept an offline write to the same key, then reconnect.
replica_ids = ["replica1", "replica2"]
r1 = Replica("replica1", replica_ids)
r2 = Replica("replica2", replica_ids)
snap = r1.put("tags", ["red"])
r2.receive_write("tags", ["red"], snap)
print(f"[r1, r2] synced on tags={r1.store['tags'][0]}")
print("=== network partition begins ===")
r1_snap = r1.put("tags", ["red", "green"])
print(f"[r1] (offline) wrote tags={r1.store['tags'][0]} -> vector {r1_snap}")
r2_snap = r2.put("tags", ["red", "blue"])
print(f"[r2] (offline) wrote tags={r2.store['tags'][0]} -> vector {r2_snap}")
print("=== network partition heals; replicas exchange writes ===")
outcome_at_r2 = r2.receive_write("tags", r1.store["tags"][0], r1_snap)
print(f"[r2] merging r1's write: {outcome_at_r2}")
print(f"[r2] final tags = {r2.store['tags'][0]}")
$ python step5_conflict_detection.py
=== Reconciling with vector clocks ===
[r1, r2] synced on tags=['red']
=== network partition begins ===
[r1] (offline) wrote tags=['red', 'green'] -> vector {'replica1': 2, 'replica2': 0}
[r2] (offline) wrote tags=['red', 'blue'] -> vector {'replica1': 1, 'replica2': 2}
=== network partition heals; replicas exchange writes ===
[r2] merging r1's write: CONFLICT DETECTED: local=['red', 'blue'] incoming=['red', 'green']
[r2] final tags = ['blue', 'green', 'red']
Vector clocks correctly flagged the concurrent write instead of silently dropping
one replica's data.
Compare that to what a naive “last write wins” merge, the kind of logic you get if you just compare wall-clock timestamps, would have done with the exact same two writes and the same 50ms clock skew from Step 1:
$ python step5_conflict_detection.py
=== What a naive last-writer-wins (wall-clock) merge would do ===
r1 wrote tags=['red','green'] at wall-clock 1790107029.657973
r2 wrote tags=['red','blue'] at wall-clock 1790107029.607973
Last-writer-wins keeps r1's value and SILENTLY DISCARDS ['red', 'blue'], with
no indication a conflict ever happened.
Last-writer-wins does not fail loudly. It just quietly throws away r2’s write and moves on, and nothing in the system ever records that a conflict happened at all. The vector-clock version catches the exact same situation and hands it back to the application (or a human) to resolve, instead of guessing.
Step 7: The Easy Mistake: Forgetting to Increment on Receive
The single easiest bug to introduce when implementing this yourself is skipping the self-increment step inside receive(). It looks harmless: you are already taking the element-wise maximum, so why bother also bumping your own counter? Build the buggy version and compare it directly against the correct one:
class BuggyVectorClock(VectorClock):
"""Identical to VectorClock, except receive() forgets to self-increment."""
def receive(self, other_counters):
merged = dict(self.counters)
for pid, value in other_counters.items():
merged[pid] = max(merged.get(pid, 0), value)
# BUG: the line that should be here is
# merged[self.process_id] = merged.get(self.process_id, 0) + 1
# Deliberately omitted.
self.counters = merged
return dict(self.counters)
$ python step6_forgotten_increment_bug.py
=== Correct VectorClock ===
[j] sends message with vector {'i': 0, 'j': 1}
[i] correct post-receive vector: {'i': 1, 'j': 1}
compare(sent, received) = 'before'
Correct: the receive event is strictly AFTER the send event.
=== Buggy VectorClock (no self-increment on receive) ===
[j] sends message with vector {'i': 0, 'j': 1}
[i] buggy post-receive vector: {'i': 0, 'j': 1}
compare(sent, received) = 'equal'
BUG: the send event and the receive event now report as 'equal': two definitely
distinct, causally ordered events (a message cannot be received before it is
sent) have collapsed onto the same vector clock value, because receiving the
message was never recorded as a step of its own.
Under the buggy version, process i’s vector after receiving j’s message is byte-for-byte identical to the vector j attached when it sent the message. That means the act of receiving, which is unambiguously a real event that happens strictly after the send, leaves no trace at all. Anywhere your system relies on “did we already process this?” or “is our state at least as new as theirs?”, this bug will quietly give you the wrong answer. Always increment your own component on receive, after merging, not instead of it.
Step 8: Know the Cost: Vector Clocks Grow With the Cluster
Vector clocks are not free. Every entry you add to the vector has to be carried on every message, forever, for as long as that process is part of the system. Measure it directly:
import sys
for n in [2, 10, 100, 1000, 10000]:
ids = [f"node-{i}" for i in range(n)]
vc = VectorClock(ids[0], {pid: i for i, pid in enumerate(ids)})
size = sys.getsizeof(vc.counters)
print(f"{n:>8} nodes | dict container size: {size:>7} bytes | {size / n:.2f} bytes/node")
$ python step7_scaling_cost.py
nodes | counters dict size (bytes) | bytes/node
======================================================
2 | 184 | 92.00
10 | 272 | 27.20
100 | 3328 | 33.28
1000 | 26032 | 26.03
10000 | 207616 | 20.76
Two caveats on that table. First, Python’s sys.getsizeof() only measures the dictionary’s own container overhead, not the strings and integers it holds, so the real memory cost per node is higher than these numbers alone suggest. Second, and more importantly, every one of those entries has to travel on every message between every pair of nodes. That cost is exactly why Wikipedia’s History section on vector clocks notes that later designs, Matrix Clocks, Plausible Clocks, Chain Clocks, exist specifically to shrink this overhead for larger, more dynamic clusters. For a cluster of a handful of nodes, plain vector clocks are simple and cheap. For thousands of nodes, the bookkeeping becomes a real, measurable cost you have to weigh against the correctness they buy you.
Verify Everything With a Test Suite
Pull every behavior above into a single pytest suite so a future change cannot silently reintroduce any of these bugs:
import pytest
from clocks import LamportClock, VectorClock, compare, concurrent_with, happened_before
from step6_forgotten_increment_bug import BuggyVectorClock
def test_lamport_orders_a_causal_pair():
a, b = LamportClock("A"), LamportClock("B")
ts_x = a.local_event()
ts_y = b.receive(a.send())
assert ts_y > ts_x
def test_vector_clock_causal_before():
vc_a = VectorClock("A", {"A": 0, "B": 0})
vc_b = VectorClock("B", {"A": 0, "B": 0})
x = vc_a.local_event()
y = vc_b.receive(vc_a.send())
assert happened_before(x, y)
def test_vector_clock_detects_concurrency():
vc_c = VectorClock("C", {"C": 0, "D": 0})
vc_d = VectorClock("D", {"C": 0, "D": 0})
for _ in range(3):
c_final = vc_c.local_event()
for _ in range(2):
d_final = vc_d.local_event()
assert concurrent_with(c_final, d_final)
def test_equal_vectors_are_equal():
assert compare({"A": 2, "B": 1}, {"A": 2, "B": 1}) == "equal"
def test_missing_keys_default_to_zero():
assert compare({"A": 1}, {"A": 1, "C": 5}) == "before"
def test_receive_without_increment_collapses_send_and_receive():
vc_j = VectorClock("j", {"i": 0, "j": 0})
vc_i = BuggyVectorClock("i", {"i": 0, "j": 0})
sent = vc_j.send()
received = vc_i.receive(sent)
assert compare(sent, received) == "equal" # the bug
def test_correct_receive_orders_after_send():
vc_j = VectorClock("j", {"i": 0, "j": 0})
vc_i = VectorClock("i", {"i": 0, "j": 0})
sent = vc_j.send()
received = vc_i.receive(sent)
assert happened_before(sent, received)
def test_send_increments_by_exactly_one():
vc = VectorClock("A", {"A": 0, "B": 0})
before = vc.local_event()["A"]
after = vc.send()["A"]
assert after == before + 1
$ python -m pytest test_clocks.py -v
test_clocks.py::test_lamport_orders_a_causal_pair PASSED [ 12%]
test_clocks.py::test_vector_clock_causal_before PASSED [ 25%]
test_clocks.py::test_vector_clock_detects_concurrency PASSED [ 37%]
test_clocks.py::test_equal_vectors_are_equal PASSED [ 50%]
test_clocks.py::test_missing_keys_default_to_zero PASSED [ 62%]
test_clocks.py::test_receive_without_increment_collapses_send_and_receive PASSED [ 75%]
test_clocks.py::test_correct_receive_orders_after_send PASSED [ 87%]
test_clocks.py::test_send_increments_by_exactly_one PASSED [100%]
============================== 8 passed in 0.04s ==============================
Common Mistakes and Gotchas
- Forgetting the self-increment on receive. Covered in Step 7. This is the single most common bug in a from-scratch vector clock implementation, and it silently erases the fact that receiving a message is its own causal event.
- Treating “before” and “concurrent” as the only two outcomes. The comparison function above has four possible results: before, after, equal, and concurrent. Code that only checks
happened_before()and assumes everything else means “concurrent” will misclassify the case where the incoming write is actually older (the “after” case), and quietly discard a legitimate new value instead of ignoring a stale one. - Using a list or a fixed-size array instead of a dictionary keyed by process ID. Real clusters add and remove nodes. A dictionary that defaults missing keys to zero handles that gracefully; a positional array does not, and either crashes or silently misaligns once node membership changes.
- Assuming vector clocks alone reconstruct a full history. A vector clock tells you the relative order (or lack of one) between two specific snapshots. It does not, by itself, store what the data was at every intermediate point. If you need that, you still need an event log or a version history; the vector clock only tells you how to sequence and compare entries in it.
How to Confirm It All Works End to End
- Run
python step1_wallclock_bug.pyand confirm it prints “BUG: wall-clock timestamps say Y happened BEFORE X.” - Run
python step2_lamport_fixes_wallclock.pyand confirm Lamport(Y) is strictly greater than Lamport(X). - Run
python step3_lamport_blind_spot.pyand confirm it demonstrates two unrelated events getting comparable Lamport timestamps. - Run
python step4_vector_clock_fixes_blind_spot.pyand confirm both assertions pass: “before” for the causal pair, “concurrent” for the unrelated pair. - Run
python step5_conflict_detection.pyand confirm it prints “CONFLICT DETECTED” for the vector-clock version and “SILENTLY DISCARDS” for the naive wall-clock version. - Run
python step6_forgotten_increment_bug.pyand confirm the correct version reports “before” while the buggy version reports “equal.” - Run
python -m pytest test_clocks.py -vand confirm all tests pass.
If every one of those checks matches, you have a working, tested implementation of both clock types, and you have personally reproduced the exact failure modes that make wall-clock timestamps unsafe for ordering distributed events.
Next Steps
Vector clocks are the foundation for a family of related techniques worth exploring next. Version vectors apply the same idea specifically to file and object versioning. Conflict-free replicated data types (CRDTs) build on causality tracking to merge concurrent updates automatically instead of just flagging them. Hybrid logical clocks combine a Lamport-style counter with a physical timestamp to get causal ordering and a value close to wall-clock time in one field.
On this site, the leader election tutorial and the two-phase commit tutorial tackle two other classic distributed-systems coordination problems from scratch in Python, and pair naturally with the causality tracking you just built here.








No Comment! Be the first one.