Time & Ordering

Lamport Clocks: Consistent With Causality, Blind to Concurrency

A single integer per node, incremented on every event and carried on every message, produces a numbering consistent with causality. The implication runs one way only, and almost every description of Lamport clocks gets that backwards.

▶ Run the lab

The question this answers

The question

Can one counter per node give me a usable ordering without any clock?

The guarantee — the property claimed, and its scope

If a → b then L(a) < L(b). The converse does not hold. L(a) < L(b) permits a → b, but it equally permits a ∥ b — the events being concurrent. A Lamport clock therefore produces an ordering *consistent with* causality, and cannot be used to detect that two events are concurrent, nor to conclude that one caused the other.

Everything below is bought to hold this sentence. "Strongly consistent" with no scope attached is a slogan, not a guarantee — read what it actually covers, and what it explicitly does not.

What a node knows — observation versus inference

A node knows its own counter and the counters that arrived on messages. From L(a) < L(b) it knows only that b is *not* in the causal past of a — a genuinely useful negative fact, and much weaker than the positive one people assume. It cannot determine, from Lamport values alone, whether two events are related at all.

A node knows its own state and the messages that arrived. Everything else is inference from evidence that was already stale. "B has not replied in five seconds" is knowledge; "B is down" is a decision — and usually the bug.

What guarantee?What does a node know?How does it work?What can fail?How does it fail?Where is coordination?What holds under failure?How does it recover?How would you know?What is the simpler thing?
lamport clockslogical timecausalityordering

The algorithm, which is three lines

Each node holds one integer, starting at zero. Three rules maintain it, and they are worth memorising because the whole result follows from them mechanically.

Why does the max work? Because receiving a message means the sender's entire causal past is now in your past too, so your counter must exceed anything in it. The max is exactly the statement "I am now later than everything the sender knew about".

That is all there is. One integer, no coordination, no clock, constant metadata regardless of cluster size. Compare that to the O(N) cost of Vector Clocks: Buying Concurrency Detection at O(N) and you can see the trade being made — and the next section is what you gave up to get it.

1L = 0 // one integer per node
2
3on local event:
4 L = L + 1
5 stamp(event, L)
6
7on send(m):
8 L = L + 1
9 m.ts = L
10
11on receive(m):
12 L = max(L, m.ts) + 1 // adopt the sender's past, then advance
13 stamp(receive_event, L)
14
15// total order (for tie-breaking): compare (L, nodeId) lexicographically.
16// This is a valid total order, and it is NOT the causal order.
The complete algorithm

The one-way implication — the fact everyone gets wrong

Here is the claim to be exact about, because it is the single most commonly misstated fact in this area:

`a → b` implies `L(a) < L(b)`. `L(a) < L(b)` does NOT imply `a → b`.

The reason is easy to see once you look for it. Two nodes that have never exchanged a message both increment their counters independently. Node P reaches 7; node Q reaches 12. Q's event has the larger number, and there is no causal relationship whatsoever between them — Q was simply busier. The number reflects *how many events that node has witnessed*, not when anything happened.

The practical consequence is severe: you cannot use Lamport values to detect a conflict. Given two versions of a key stamped 7 and 12, you cannot tell whether version 12 was written by someone who had seen version 7 (in which case it supersedes it) or by someone who had never heard of it (in which case they conflict and both should be preserved). Choosing 12 because it is bigger is Last Write Wins Is Data Loss You Chose by Default with a logical clock instead of a physical one — the same silent data loss, dressed up in respectable notation.

What the implication *does* give you, contrapositively, is a real fact: if L(b) ≤ L(a) then b cannot have happened before... no. Be careful. From a → b ⟹ L(a) < L(b) you get: if L(a) ≥ L(b), then not a → b. So a Lamport value can definitively *rule out* one direction of causality. That is a useful negative, and it is the only definite thing you get.

L(q3) > L(p1), and yet the two are concurrentprotocol
Node PNode Qp1: L=1 write x (write) at t=1p1: L=1 write xq1: L=1 (write) at t=2q1: L=1q2: L=2 (write) at t=4q2: L=2q3: L=3 write x (write) at t=6q3: L=3 write xt=1time →t=6
delivereddelayed (dashed, long)duplicated (×2)dropped — stops short, never arriveswrite
No messages pass between P and Q, so p1 ∥ q3 — neither could have influenced the other. Yet L(q3)=3 > L(p1)=1. A merge rule that keeps the higher Lamport value discards p1 without noticing there was a conflict at all.

What it is actually good for

Given that limitation, why does anyone use them? Because "an order consistent with causality, for one integer and zero coordination" is genuinely valuable in the cases where you need *an* order and do not need to detect conflicts.

The canonical use is tie-breaking into a total order. Comparing (L, nodeId) lexicographically gives a total order over all events that never contradicts causality: if a → b, a sorts first. So you get a deterministic global sequence that every node computes identically, without agreement. Lamport's original paper used precisely this to build a distributed mutual-exclusion algorithm. Note carefully what this total order is not: it is not the real order, since it orders concurrent events arbitrarily. It is *a* consistent order, and for many purposes that is exactly enough.

Other honest uses: as a monotonic version stamp so a replica can discard an update it has already seen or superseded; as a cheap way to make identifiers sortable in a causally-sensible way; as the counter component of a hybrid logical clock, which pairs a physical timestamp with a Lamport counter so that the value stays close to wall time while still never contradicting causality — the practical middle ground many databases actually ship.

And one more, which is more about you than about the system: Lamport clocks are the right mental model for *why* logical time exists at all. Once you have understood that a number can encode "later in the causal sense", vector clocks are a small step rather than a new idea.

ObservationValid conclusionInvalid conclusion
L(a) < L(b)protocolb is not in a's causal pasta happened before b
L(a) = L(b)protocola and b are concurrent (different nodes)a and b are simultaneous in real time
L(a) > L(b)protocola is not in b's causal pasta is more recent in any physical sense
Sorting all events by (L, node)protocolA deterministic total order that never contradicts causalityThe order in which things really happened
What you can and cannot conclude from Lamport values

Why one integer cannot possibly be enough

There is a clean information-theoretic reason the converse fails, and it is worth internalising because it generalises.

Causality is a partial order, and a partial order over events on N nodes is not embeddable in the integers without loss. The integers are totally ordered: for any two, one is smaller. So any mapping from events to integers must assign an order to every pair — including pairs that are genuinely unordered. The mapping is forced to invent, and the inventions are indistinguishable from the real orderings once you only have the numbers.

To recover concurrency you need a structure that can itself be partially ordered — one that permits "neither is greater". A vector of N integers compared component-wise is exactly such a structure, which is why Vector Clocks: Buying Concurrency Detection at O(N) work and why they cost O(N) rather than O(1). The cost is not an implementation detail; it is the price of representing a partial order faithfully.

This also tells you when a Lamport clock is sufficient: whenever the underlying order really is total. A single-partition log, a single-leader replication stream, a per-key sequence maintained by one owner — in all of these, program order at the owner already gives a total order, and a counter is a faithful encoding rather than a lossy one.

Key points

  • One integer per node: increment on every event, attach to every message, max on receive then increment.
  • a → b implies L(a) < L(b). The converse is false, and assuming it is the standard error.
  • The only definite conclusion from L(a) ≥ L(b) is that a did not happen before b.
  • Lamport values cannot detect concurrency, so they cannot detect conflicts — using them to pick a winner is LWW with better notation.
  • (L, nodeId) gives a deterministic total order consistent with causality, which is what they are genuinely good for.
  • One integer cannot encode a partial order without loss; that is a structural fact, not an implementation gap.

The chain, answered

Every field here is required, which is why no lesson in this domain can recommend a design without naming what an operator sees when it fails, what survives the partition, what repairs it afterwards, and the simpler thing to consider first.

How it works
  • Every node keeps a single counter, initially zero.
  • Before any local event, the node increments its counter and stamps the event.
  • Every outgoing message carries the sender's counter value.
  • On receipt, the node sets its counter to max(local, received) and then increments, stamping the receive event.
  • Any two events can then be compared by (counter, nodeId), yielding a total order that never contradicts happens-before.
What can fail at the boundary
  • A message loses its timestamp at a boundary, and the receiver's counter fails to advance past the sender's past.
  • A node restarts with a counter reset to zero and starts emitting values that appear to precede events it actually follows.
  • A counter is persisted lazily, so a crash rolls it back and duplicate values are emitted for different events.
  • The counter overflows a narrow integer type in a long-lived, high-throughput system.
  • Someone compares Lamport values from two different systems, or from two counters with different scopes, where the numbers have no relationship at all.
How it fails — what an operator sees
  • Silent lost update: two replicas take concurrent writes, the merge keeps the higher Lamport value, and the busier node's writes systematically win. The operator observes that edits made from one region "do not stick", with no conflict recorded and no error.
  • Order regression after a restart: a node whose counter reset emits stamps below values it previously issued, and downstream consumers that discard non-increasing versions silently drop that node's updates until the counter catches up. The operator sees one node's traffic vanish from a materialised view.
  • Ties broken inconsistently: two components tie-break (L, nodeId) differently — one by string comparison, one numeric — and two replicas converge to *different* winners. The operator sees a key that never stops disagreeing between replicas.
  • False confidence in an audit trail: an investigator uses Lamport values as a chronology and concludes one action caused another when the two were unrelated. The failure surfaces as a wrong root-cause conclusion, not as a system error.
Where coordination is required
  • None. This is the whole point: the ordering is derived from information already flowing, and no node ever waits for another.
  • The total order obtained by tie-breaking is agreed *without* agreement — every node computes the same sequence from the same stamps, which is why Lamport clocks appear inside algorithms that would otherwise need consensus for sequencing alone.
  • Anything requiring the order to be *decided* rather than *computed* — such as knowing when the order is stable and no earlier event can still arrive — does need coordination, and that is Total Order Broadcast Is Consensus Wearing a Different Hat.
What still holds under failure
  • Counters keep advancing during a partition; both sides produce valid, non-contradictory stamps.
  • After healing, all stamps remain comparable and the derived total order remains consistent with causality.
  • What does not survive: any belief that the numbers reflect physical time, or that a larger number means a later real-world event.
How it recovers
  • Detect: alert on a node emitting a stamp lower than one it previously emitted — the direct signal for a counter reset.
  • Contain: persist the counter durably before emitting a stamp derived from it, so a crash never rolls it backwards.
  • Recover: on restart, initialise the counter above the highest value observed anywhere for that scope, rather than from zero.
  • Reconcile: where merges used Lamport order to pick winners, the losing versions are gone — the recovery is to stop doing that and keep siblings instead (Version Vectors: Making the Conflict Visible).
  • Verify: replay a known causal chain and assert stamps increase along every edge.
How you would know
  • Per-node counter value over time; a flat or decreasing line is a persistence or reset bug.
  • Distribution of counter values across nodes. Wide divergence means the nodes barely communicate, which means most pairs are concurrent and Lamport ordering is doing very little for you.
  • Rate at which incoming messages advance the local counter via the max branch — a proxy for how much causal information is actually flowing.
  • Count of updates discarded as "not newer", which is where silent loss would show up if it shows up anywhere.
When it helps
  • When you need a deterministic total order that never contradicts causality and cannot afford consensus for sequencing.
  • As a version stamp on a stream that already has a single owner, where the true order is total anyway.
  • As the logical component of a hybrid logical clock, keeping stamps near wall time without ever contradicting causality.
  • For deduplication and idempotent application of updates where "have I already applied something at least this recent" is the question.
When it hurts
  • Any time you must decide whether two writes conflict. Lamport clocks structurally cannot answer that, and using them anyway loses data quietly.
  • Anywhere the value is presented as a time to a human, who will read it as one.
  • In systems where nodes rarely exchange messages, the counters drift into meaninglessness — large differences with no causal content.
Simpler alternatives
  • Use Vector Clocks: Buying Concurrency Detection at O(N) when you need to detect concurrency; pay O(N) metadata for the ability to see conflicts.
  • Use Version Vectors: Making the Conflict Visible when the question is per-object rather than per-event, which is usually the case for replicated data.
  • Use a hybrid logical clock when stamps must stay close to physical time for humans and external systems, while remaining causally sound.
  • Use a single sequencer when a genuine total order is required and you can afford the coordination point: Distributed Uniqueness: One Name, Many Shards.
  • Use no logical clock at all where a single owner already sequences every write — its program order is already a total order.

L(a) < L(b) does not mean a happened before b

Lamport clocks: consistent with causality, blind to concurrency
One integer per node. Increment on every event, attach it to every message, take the max on receive. Then look at what the number does and does not prove.
Scenario
P
Q
R
L = 0
on local event:  L = L + 1
on send(m):      L = L + 1; m.ts = L
on receive(m):   L = max(L, m.ts) + 1
#NodeEventLPick
1Pwrite x1a
2Qwork1
3Qwork2
4Qwrite x3b
the numbers say
L(a) < L(b)
causality says
concurrent
misleading pairs in this trace
2
safe conclusion
b is not in a’s past
L(a) < L(b), and yet these two events are concurrent — no chain of messages connects them in either direction. Q was simply busier. If you resolve a conflict by keeping the larger Lamport value you have just discarded a write that never lost anything: that is last-write-wins in respectable notation, with the busiest node always winning instead of the fastest clock.
The trace, with each event’s Lamport value.protocol
PQRL=1 write x (decide) at t=0L=1 write xL=1 work (write) at t=1L=1 workL=2 work (write) at t=2L=2 workL=3 write x (decide) at t=3L=3 write xt=0time →t=3
delivereddelayed (dashed, long)duplicated (×2)dropped — stops short, never arriveswritedecide
A message forces the receiver’s counter above everything in the sender’s past. Nothing forces two nodes that never speak to have related numbers.
Sorted by (L, nodeId) — a deterministic total order that never contradicts causality
P·1Q·1Q·2Q·3
That total order is what Lamport clocks are genuinely good for: every node computes the same sequence, with no coordination and one integer of metadata. It is a consistent order, not the real one — it orders concurrent events arbitrarily. One integer cannot do better: causality is a partial order, the integers are totally ordered, so the mapping is forced to invent relations, and the inventions are indistinguishable from the real ones once you only have the numbers. Recovering concurrency needs a structure that can itself be partially ordered — which is the next widget, and the O(N) it costs.
protocolThe rules and the implication are exact: a → b ⟹ L(a) < L(b), and the converse is false. The causal truth shown beside each comparison is read from vector clocks maintained alongside, which encode happens-before exactly.

What people believe, and what is true

Claim

If L(a) < L(b) then a happened before b.

Reality

The implication runs the other way only. Two unrelated events on different nodes routinely have different Lamport values, and the busier node always has the larger one.

Claim

Lamport clocks let you detect conflicting writes.

Reality

They cannot. Detecting concurrency requires a structure that can be partially ordered; a single integer is totally ordered and therefore lossy by construction.

Claim

Equal Lamport values mean simultaneous events.

Reality

Equal values mean the events are concurrent — neither influenced the other. They may be far apart in real time.

Claim

Lamport clocks are just a worse version of vector clocks.

Reality

They are a different point on a real trade-off: O(1) metadata and a usable total order, versus O(N) metadata and conflict detection. Plenty of systems correctly choose the first.

Go deeper

Only the levels this lesson can honestly fill — a missing level is a claim nobody had.

Overview

One counter per node, bumped on every event and maxed on every receive. If a caused b, a's number is smaller. A smaller number does not mean it caused anything.

Practical

Use them for a deterministic total order via (L, nodeId), for deduplication, and as a version stamp on single-owner streams. Never use them to choose a winner between replicas — that is silent data loss. Persist the counter before you emit stamps derived from it.

Advanced

The converse fails because the integers are totally ordered and causality is not; any O(1) encoding of a partial order must invent orderings, and the inventions are indistinguishable from real ones afterwards. Hybrid logical clocks recover human-meaningful values by pairing a physical component with the counter, bounding the divergence from wall time while preserving a → b ⟹ HLC(a) < HLC(b).

Apply it

Build it, then break it
  • 🔧 Implement the three rules and produce a spacetime diagram where the higher Lamport value belongs to the causally *earlier*-looking event.
  • 🔧 Take an existing "version" integer in your system and determine whether it encodes a genuine total order or is silently discarding concurrent writes.
Reason about this
  • A team proposes replacing timestamps with Lamport counters to fix a lost-update bug. Will that fix it? Explain.
  • After a node restart, one replica's updates stop appearing downstream for several hours and then resume. What single metric would have shown this immediately?
Interview questions
  • 💬 State precisely what a Lamport clock guarantees, and what it does not.
  • 💬 Given two replicas holding versions of a key stamped 7 and 12, can you conclude 12 supersedes 7? Why not?
  • 💬 How would you build a total order over events without consensus, and what is the catch?