Chapter 04 · Time, Ordering, Logical Clocks, and Versioning

Lamport Clocks and Happens-Before Reasoning

Build Lamport clocks from local events and messages, preserving happens-before while exposing the limit of scalar logical timestamps.

Intermediate90–110 minutesLamport clock + happens-before traceVendor-neutral · Python stdlibLogical time, not UTCLast reviewed: August 2026

Learning outcomes

AtlasMart now needs an ordering marker that survives wall-clock skew. The goal is not to know “what time it really was,” but to ensure that if one event could have influenced another, the second event receives a greater logical timestamp. Lamport's clock construction does exactly that with one integer per process and a simple rule on message receipt.

01

Define the happens-before relation from local program order, message send/receive, and transitivity.

02

Implement Lamport logical clocks for local events, sends, and receives.

03

Prove from a trace that happened-before implies increasing Lamport timestamps.

04

Explain why the converse is false: timestamp order does not prove causality or detect concurrency.

05

Use a node-ID tie-breaker for deterministic total order while labeling it as an imposed order, not observed causality.

1. Happens-before is a partial order

Lamport writes the relation as an event a “happened before” event b when at least one of three conditions holds: they occur in the same process and a precedes b; a is the send of a message and b is its receipt; or the relation follows transitively through other events. If neither a → b nor b → a is established, the events are concurrent with respect to this model.

This is a partial order because some pairs have no causal relation. That is not missing data to be guessed from wall-clock timestamps. It is a meaningful statement: the system has no message/program-order evidence that either event influenced the other.

2. Lamport's logical-clock rules

Situation Rule Reason
Local event increment local counter each event advances its process history
Send message increment and attach the resulting counter the message carries the sender’s causal progress
Receive message with timestamp m set local counter to max(local,m)+1 receipt must be later than both local history and the send timestamp

If clocks obey these rules, then a → b implies C(a) < C(b). This is the key guarantee. The number is a logical marker; units such as milliseconds are meaningless.

3. Trace one AtlasMart order through two nodes

Node A records a local event, then sends an order. Node B receives it and reserves stock. The receive rule forces B's clock beyond the send timestamp even if B had processed fewer local events before the message. A third node C edits profile preferences without any communication path to A or B.

text · causal timeline
A: A1 local [L=1] -> A2 send order [L=2] ----message---->
B:                                             B1 receive [L=3] -> B2 reserve [L=4]
C: C1 profile [L=1] -> C2 preferences [L=2]

Known: A2 -> B1 -> B2
Unknown: B2 vs C2 (no causal path)

4. The clock condition is one-way

The common mistake is to invert the theorem. From C(a) < C(b) you cannot conclude a → b. Independent processes increment their own counters, so concurrent events can receive different logical values. Even equal values on two different nodes do not imply they are the same event.

A system can extend Lamport timestamps to a deterministic total order with a tie-breaker such as (logical_timestamp, node_id). That can be useful for replay, logs, lock queues, or deterministic conflict resolution. But the tie-breaker manufactures order between concurrent events; it does not discover a hidden causal relation.

Precision rule

Lamport clocks preserve causal precedence but do not identify concurrency. Lesson 3 adds per-actor information so incomparable vectors can expose concurrent branches.

5. Deliberately wrong approach — reject every smaller Lamport timestamp as “older causally”

Imagine AtlasMart receives two independent profile edits: one from node B at logical time 4 and one from node C at logical time 2. A naive merge keeps only B because 4 is greater. Nothing in the clock says B observed C or superseded it. The rule silently converts an arbitrary counter order into conflict semantics.

The repair is to choose metadata appropriate to the question. If you only need an order compatible with causality, Lamport clocks are sufficient. If you need to determine whether one version descended from another or whether two versions are concurrent, a vector-style causal context can encode more information.

6. AtlasMart lab — implement Lamport clocks

python · lamport_clock.py
from dataclasses import dataclass

@dataclass
class Node:
    name: str
    clock: int = 0

    def local(self, label):
        self.clock += 1
        return (label, self.name, self.clock)

    def send(self, label):
        self.clock += 1
        return (label, self.name, self.clock)

    def receive(self, label, remote_clock):
        self.clock = max(self.clock, remote_clock) + 1
        return (label, self.name, self.clock)

A, B, C = Node("A"), Node("B"), Node("C")

history = []
history.append(A.local("A1 local"))
msg = A.send("A2 send order")
history.append(msg)
history.append(B.receive("B1 receive order", msg[2]))
history.append(B.local("B2 reserve stock"))
history.append(C.local("C1 independent profile edit"))
history.append(C.local("C2 independent preference edit"))

print("Lamport timestamps")
for label, node, ts in history:
    print(f"{label:31} node={node} L={ts}")

print("\ncausal chain:")
print("A2 send order -> B1 receive order -> B2 reserve stock")
print("timestamps:", msg[2], "<", history[2][2], "<", history[3][2])

print("\nconcurrency warning:")
print("B2 has L=", history[3][2], "and C2 has L=", history[5][2])
print("numeric order exists, but there is no message path proving B2 happened-before C2 or vice versa")

total_order = sorted(history, key=lambda x: (x[2], x[1]))
print("\ndeterministic total order using (Lamport, node-id):")
for x in total_order:
    print(x)
print("this tie-broken total order is useful for deterministic ordering, not proof of causality")

Expected output

text · verified deterministic output
Lamport timestamps
A1 local                        node=A L=1
A2 send order                   node=A L=2
B1 receive order                node=B L=3
B2 reserve stock                node=B L=4
C1 independent profile edit     node=C L=1
C2 independent preference edit  node=C L=2

causal chain:
A2 send order -> B1 receive order -> B2 reserve stock
timestamps: 2 < 3 < 4

concurrency warning:
B2 has L= 4 and C2 has L= 2
numeric order exists, but there is no message path proving B2 happened-before C2 or vice versa

deterministic total order using (Lamport, node-id):
('A1 local', 'A', 1)
('C1 independent profile edit', 'C', 1)
('A2 send order', 'A', 2)
('C2 independent preference edit', 'C', 2)
('B1 receive order', 'B', 3)
('B2 reserve stock', 'B', 4)
this tie-broken total order is useful for deterministic ordering, not proof of causality

The trace verifies the clock condition on the message chain. It also prints a deterministic total order that includes C's independent events. That last list is useful only because the code declares a tie-break rule; it must not be read as a causal proof.

Verification checklist

  • Every local/send event increments its node clock.
  • B's receive timestamp is greater than A's send timestamp.
  • The printed causal chain has strictly increasing logical values.
  • The lesson explicitly marks B2 and C2 as causally unproven/concurrent despite their numeric ordering.
  • No wall-clock API is needed.

Check your understanding

  1. What three rules define happens-before?
  2. What does the Lamport clock condition guarantee?
  3. Why does C(a) < C(b) not prove a happened-before b?
  4. What does a (Lamport,node-id) tie-breaker add?
  5. When would Lamport clocks be insufficient for conflict handling?
Review the answers

Local process order, message send-before-receive, and transitive closure.

If a happened-before b, then the logical timestamp of a is smaller than the logical timestamp of b.

Concurrent processes can advance counters independently, producing an arbitrary numeric order without a message/program-order dependency.

It creates a deterministic total order among all events, including concurrent ones; that order is imposed rather than causal evidence.

When the application must distinguish an ancestor version from a concurrent branch instead of merely producing an order consistent with known causality.

7. Production judgment and next bridge

Lamport clocks are compact and powerful when the requirement is causal-compatible ordering: “receives must sort after sends,” “state-machine metadata must advance,” or “events need a deterministic tie-broken order.” They do not solve clock synchronization, external real-time ordering, bounded staleness, or concurrent-version detection.

Lesson 3 makes the tradeoff explicit. Version vectors carry one logical counter per actor/replica identity. The additional metadata lets two versions be compared component-wise, so the system can distinguish ancestor, descendant, equal, and concurrent states.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.