Lamport Timestamps: Logical Clocks for Distributed Systems
Lamport timestamps order events across machines with a simple counter instead of wall clocks. How the algorithm works, what it guarantees, and its limits.
Lamport timestamps are a way to order events in a distributed system using a simple integer counter on each machine instead of physical clocks. Each process increments its counter for every event and attaches it to every message it sends; on receiving a message, a process jumps its counter ahead of the sender’s. The result is a numbering that never contradicts cause and effect: if one event could have influenced another, the first always gets the smaller timestamp. Leslie Lamport introduced the idea in 1978, and it remains one of the most cited concepts in distributed computing.
Why wall clocks are not enough
The obvious way to order events across machines is to stamp each one with the current time. The problem is that every machine’s clock is slightly wrong. Clocks drift, synchronization protocols correct them in jumps, and a leap-second adjustment or a misconfigured server can push a clock backward.
If machine A sends a message at 10:00:00.005 by its clock and machine B receives it at 10:00:00.003 by its own, sorting by timestamp says the reply happened before the question. For anything that depends on order, such as replicating writes, resolving conflicts, or debugging a trace, that is a correctness bug.
Lamport’s insight was that most systems do not actually need to know when something happened. They need to know what could have caused what.
The happened-before relation
Lamport defined a relation written a → b, read “a happened before b.” It holds when:
aandbare events in the same process, andacame first, orais the sending of a message andbis the receipt of that same message, or- there is some event
cwitha → candc → b(the relation is transitive).
If neither a → b nor b → a, the two events are concurrent. Neither could have influenced the other, because no chain of messages connects them. Concurrency here is about information flow, not about whether the events overlapped in real time.
The algorithm
Each process keeps a single integer counter, starting at zero:
- Before each local event, increment the counter.
- When sending a message, increment the counter and include its value in the message.
- When receiving a message carrying timestamp
t, set the counter tomax(counter, t) + 1.
Here is a short trace with two processes:
P1: event a -> counter 1
P1: send m -> counter 2 (m carries 2)
P2: event x -> counter 1
P2: receive m -> max(1, 2) + 1 = 3
P2: event y -> counter 4
The receive rule is what makes it work. Because P2 jumps past the sender’s value, everything P2 does after receiving m gets a higher number than everything P1 did before sending it.
What Lamport timestamps guarantee
The core guarantee is the clock condition: if a → b, then L(a) < L(b).
Note the direction. The converse does not hold. L(a) < L(b) does not tell you that a happened before b. In the trace above, x on P2 has timestamp 1 and a on P1 also has timestamp 1, and P1’s send has timestamp 2, yet x and the send are concurrent. Lamport timestamps can order concurrent events, but they cannot detect that the events were concurrent.
Building a total order
Many systems need every node to agree on a single sequence of events, even for concurrent ones. Lamport timestamps get you most of the way. To break ties between equal counters, append the process ID: compare (timestamp, process_id) pairs lexicographically. Every event now has a unique position, and the resulting total order still respects causality.
Lamport’s original paper used exactly this to build a distributed mutual exclusion algorithm: processes broadcast lock requests stamped with Lamport times, and the request with the lowest (timestamp, id) wins. The same idea appears in replicated state machines, where every replica applies commands in the same agreed order.
Lamport timestamps vs vector clocks
The limitation of Lamport timestamps is the motivation for vector clocks, which track one counter per process instead of one counter total.
| Lamport timestamps | Vector clocks | |
|---|---|---|
| Size per event | One integer | One integer per process |
| If a happened before b | L(a) < L(b) | V(a) < V(b) |
| If L(a) < L(b) or V(a) < V(b) | Cannot conclude a happened before b | a happened before b |
| Detects concurrency | No | Yes |
| Total order | Yes, with process-ID tie-break | Only a partial order |
| Typical use | Ordering logs, mutual exclusion, versioning | Conflict detection in replicated data |
Choose Lamport timestamps when you need a cheap, compact order that respects causality. Choose vector clocks when you must know whether two updates conflicted, such as when two replicas accept writes to the same key independently.
Where you see them in practice
The pattern shows up under different names across real systems:
- Consensus protocols. The term numbers in Raft behave much like Lamport clocks: nodes adopt the highest term they see and reject messages from older terms, which keeps a stale leader from overriding a newer one.
- Replicated data types. Last-writer-wins registers in a CRDT often use a Lamport timestamp with a node-ID tie-break to decide which concurrent write survives, so all replicas converge on the same value.
- Hybrid logical clocks. Some distributed databases combine physical time with a Lamport-style counter. Timestamps stay close to wall-clock time for human readability, while the counter preserves causal order when clocks disagree.
- Membership and gossip. Version counters attached to state in a gossip protocol let nodes tell which piece of information is newer without trusting each other’s clocks.
Common pitfalls
A few mistakes come up repeatedly:
- Treating the order as real time. A Lamport timestamp says nothing about elapsed time. Event 50 might have occurred milliseconds or hours after event 49.
- Assuming smaller means earlier. Remember the clock condition only runs one way. Two events with different timestamps may still be concurrent.
- Forgetting persistence. If a process restarts and resets its counter to zero, it can issue timestamps that go backward. Persist the counter, or restart from a value safely above anything previously issued.
The takeaway
Lamport timestamps replace unreliable wall clocks with a counter that increments on every event and jumps forward on every received message. They guarantee that causes always get smaller numbers than their effects, and with a process-ID tie-break they give every node the same total order. What they cannot do is tell you whether two events were concurrent. When you need that, step up to vector clocks; when you just need a consistent, causality-respecting order, a single integer per process is hard to beat.
Keep reading
The Lycoris Team · · 5 min read Kadane's Algorithm Explained: Maximum Subarray in O(n)
Kadane's algorithm finds the maximum-sum contiguous subarray in one O(n) pass. How it works, why it's correct, code, edge cases, and common variations.
Chisato · · 5 min read Gossip Protocols Explained: How Clusters Share State
A gossip protocol spreads information through a cluster by having each node periodically share state with a few random peers. How it works and its trade-offs.
Chisato · · 4 min read Byzantine Fault Tolerance Explained
Byzantine fault tolerance lets a distributed system keep working correctly even when some nodes fail arbitrarily or actively lie, not just crash cleanly.