Engineering Atlas › System Design Fundamentals

Ordering events with Lamport timestamps

12 min read · last reviewed 2026-10-17

Open in the interactive app: practice, quiz, mock interview →

In one minute

How it works

The three rules

Each process p holds an integer C, starting at 0.

  1. Before any local event, increment C.
  2. When sending a message, increment C and attach it to the message.
  3. When receiving a message carrying T, set C = max(C, T) + 1 and stamp the receive event with that value.

That is the whole algorithm, close to the two implementation rules in Lamport's 1978 paper (Lamport). The max step is the important part: a receiver jumps ahead of anything the sender had seen, so an effect always gets a larger number than its cause.

Why the guarantee holds

Happens-before is built from two links: program order on one process and send-to-receive across processes. Rule 1 makes timestamps strictly increase along program order. Rules 2 and 3 make the receive larger than the send. Any causal chain is a sequence of those links, so timestamps strictly increase along it.

Why the converse fails

Two processes that never communicate both count 1, 2, 3. Event 2 on one and event 3 on the other are unrelated, but the numbers suggest an order. Lamport timestamps compress a partial order into a line, and that throws away the information about which pairs were unordered. If you need to detect concurrency, use vector clocks, which Fidge and Mattern each published in 1988 for exactly this gap (Mattern).

There is a second blind spot. The clocks only see messages that go through the system. Lamport's own paper describes a user who issues request A, then phones a friend who issues request B; the system may stamp B before A, because the phone call carried causality the clocks never saw (Lamport 1978). Any side channel, a shared cache, a queue that drops headers, a human, can do the same.

Building a total order

Break ties with process ID: (C_a, p_a) < (C_b, p_b) if C_a < C_b, or the counters are equal and p_a < p_b. Every process applies the same rule to the same timestamps and reaches the same order without exchanging extra messages. The order is arbitrary among concurrent events but never contradicts causality.

What it is used for

Mutual exclusion. Each process broadcasts a timestamped request. A process enters the critical section when its request is the smallest in its queue and it has heard a later-stamped message from every other process. It costs 3(N-1) messages per entry and assumes reliable FIFO channels and no crashes, so it is a teaching tool more than a production lock.

Totally ordered broadcast. Deliver messages in timestamp order once you know no smaller timestamp can still arrive, which requires hearing from every process. Replicas that apply operations in that order stay identical.

Epochs and terms. Raft terms, Kafka leader epochs and ballot numbers in Paxos follow the same idea: a counter that only goes up, where seeing a larger value makes you adopt it. They are Lamport clocks specialised to leadership changes.

Practical details

Use 64-bit counters. A busy process incrementing a million times per second would take over 290,000 years to overflow. Persist the counter, or restart above any value you might have sent, or a restarted process may reuse numbers. Clocks only advance on events, so the counter carries no meaning about elapsed time.

Down the stack

Layer 1The contract

A Lamport clock implementation owes its callers three testable properties. First, the clock condition: for every send and its receive, and for consecutive events on one process, the later stamp is strictly larger. Second, (counter, pid) is a strict total order, so two distinct events never compare equal, which requires process IDs to be unique and stable. Third, monotonicity across restarts: no process ever issues a stamp it issued before. The first two hold by construction; the third is an engineering problem that most bugs in this area come from.

Totally ordered broadcast built on top adds a delivery contract: every correct process delivers the same messages in the same order. It assumes reliable FIFO channels and no crashes, and those assumptions are what production systems remove by switching to a leader.

Layer 2The mechanism

Most production uses of the idea are terms and epochs. In Raft every RPC carries the sender's term; a receiver that sees a larger term sets currentTerm to it and becomes a follower, which is rule 3 with adoption instead of max-plus-one. The Raft paper says outright that terms act as a logical clock (Raft paper). etcd persists currentTerm inside a HardState record (Term, Vote, Commit) in its write-ahead log before acting on it (raft.proto).

Kafka writes a 32-bit PartitionLeaderEpoch into the header of every record batch, and each broker keeps a leader-epoch-checkpoint file mapping epochs to the first offset written in that epoch. After a failover, a follower asks the new leader for the end offset of its last epoch (the OffsetForLeaderEpoch request, schema) and truncates any divergent tail (KIP-101). The epoch field is visible in the record batch format as partitionLeaderEpoch: int32 (Kafka docs). ZooKeeper's zxid puts the epoch in the high 32 bits and a per-epoch counter in the low 32 bits, and a new leader starts at (e+1, 0) where e is the highest epoch it has seen, which is rule 3 again (ZooKeeper internals).

Paxos ballot numbers are the clearest example: a ballot is literally (round, node_id), compared lexicographically. A proposer that is rejected because it saw a higher ballot picks a round above it, which is the same max-then-increment rule, and the node ID in the low position is the pid tie-breaker.

Layer 3The machine

In memory the clock is an 8-byte integer. A local event is a single atomic increment, a LOCK XADD on x86 costing a few nanoseconds when uncontended. A receive needs max(C, T) + 1, and x86 has no atomic max, so a lock-free implementation uses a compare-and-swap loop that retries when another thread advanced the counter first. Under contention, every core is fighting for one 64-byte cache line.

Persistence is the expensive part. An fsync costs tens of microseconds on an SSD with power-loss protection and several milliseconds on many network volumes, far too slow to pay on every event. The standard trick, also used by database sequence caches, is to persist a high-water mark ahead of use: write "C may reach 10,000" once, issue stamps up to that limit from memory, and on restart begin from the persisted mark. A crash wastes the unused range but never reuses a value.

On the wire the cost is tiny. Protobuf encodes integers as varints, 7 payload bits per byte, so a counter under 16,384 takes 2 bytes and a full 64-bit value at most 10 (Protobuf encoding). The FIFO assumption comes from TCP, but only within one connection. After a reconnect, an application-level retry can deliver an older message after a newer one, so real protocols add per-channel sequence numbers or, as Raft does, include enough log position information to reject stale messages.

Why it's built this way

  1. There is no shared clock across machines, so ordering must be carried in the messages themselves.
  2. A single integer is the smallest possible carrier and fits in a few bytes, so it is cheap enough to attach to every message.
  3. A single integer cannot represent a partial order, so concurrency information is lost and ties need an arbitrary but agreed rule, the pid.
  4. The guarantee depends on the counter never going backwards, and memory is lost on crash, so counters are persisted, usually in reserved blocks to amortise the cost of fsync.

Diagram

sequenceDiagram
    participant P1
    participant P2
    participant P3
    Note over P1: e1 local, C=1
    P1->>P2: send, C=2
    Note over P3: f1 local, C=1
    Note over P2: receive, C=max(0,2)+1=3
    P2->>P3: send, C=4
    Note over P3: receive, C=max(1,4)+1=5
    Note over P1: e3 local, C=3
    Note over P1,P3: e3 (3) and P2 receive (3) tie, broken by pid

Worked example

Three replicas of a key-value store use totally ordered broadcast with Lamport timestamps to apply SET operations in the same order. Replica 1 sends SET x=1 stamped (5, 1). Replica 3, whose counter is 4, concurrently sends SET x=7 stamped (5, 3). Every replica buffers both. Once each has heard something stamped above 5 from all three replicas, it delivers (5, 1) then (5, 3). Everyone ends with x=7.

The cost shows up in latency. A replica cannot deliver until it has heard from every peer, so delivery waits for the slowest replica. With peers 2 ms apart, plus heartbeats every 10 ms to keep silent replicas from stalling delivery, expected delivery delay is around 10 to 12 ms, and a single crashed replica blocks delivery entirely. This is why production systems usually replace the "hear from everyone" rule with a leader and a quorum, keeping the monotonic counter as the term.

Overhead is about 12 bytes per message, constant regardless of cluster size.

In the wild

Trade-offs

ApproachStrengthsWeaknessesChoose when
Lamport timestampsConstant size, simple, causalNo concurrency detectionTotal ordering, epochs, tie-breaking
Lamport plus pid total orderIdentical order on all nodesOrder among concurrent events is arbitraryDeterministic replicated state
Vector clocksDetect concurrency exactlySize grows with processesConflict detection
Hybrid logical clocksCausal and near real timeNeeds bounded skewTimestamps users and queries depend on
Physical timestampsMeaningful to humansCan contradict causalityDisplay and coarse logging only

Pitfalls in production

Leadership lens

Check yourself

  1. Process A has counter 9 and receives a message stamped 4. What is its new counter?

    Show answer

    max(9, 4) + 1 = 10. The receive is still an event, so it increments.

  2. Two events have timestamps (7, 2) and (7, 1). Could one have caused the other?

    Show answer

    No. If one caused the other, its counter would be strictly smaller. Equal counters mean they are concurrent.

  3. Why is a Raft term a kind of Lamport clock?

    Show answer

    It only increases, it travels in every message, and a node that sees a higher term adopts it, which is rule 3 applied to leadership.

Sources and further study

Primary

Systems docs

Books

Other curricula

Watch

Free resources

Open in the interactive app: practice, quiz, mock interview →