WierdX — Programming Reference All tutorials →
Developer reference · Practical tutorials · CS fundamentals
Distributed Systems

Vector Clocks and Logical Time: Ordering Events in Distributed Systems

Two machines rarely agree on what time it is, down to the millisecond, and even a few milliseconds of drift is enough to misorder events that a system genuinely needs to order correctly. Logical clocks solve this by giving up on wall-clock time entirely.

Published July 6, 2026

Machine clocks drift, even with NTP synchronization running continuously; a few milliseconds of skew between two servers is normal, not a bug. That's harmless for most purposes, but it becomes a real problem the moment you need to know, with certainty, which of two events on different machines happened first — if machine A's clock is running fast, an event it stamps might show a timestamp earlier than an event on machine B that actually happened before it. Wall-clock timestamps simply can't be trusted as the ground truth for ordering across machines.

Lamport clocks: order without a shared clock

The simplest fix, a Lamport clock, has each process keep a single counter. On every local event, it increments its counter. On every message it sends, it attaches the current counter value. On every message it receives, it sets its own counter to one more than the max of its current value and the value it just received. This guarantees that if event A caused event B (A happened, then a message carrying A's counter arrived, then B happened), A's counter is strictly less than B's. That's useful, but it only works in one direction: seeing that A's counter is less than B's doesn't tell you A actually caused B, only that A's counter was smaller — two completely unrelated events on different machines can end up with A's counter smaller than B's purely by coincidence.

Vector clocks close that gap

A vector clock gives each process its own slot in a shared array rather than a single shared counter. With three processes, each event carries a vector like [2, 0, 1], where each position is that process's own view of how many events it has observed from each participant, including itself.

// process P1 sends a message after 2 local events
vector = [2, 0, 0]

// process P2 receives it, merges component-wise with its own [0, 1, 0]
merged = [max(2,0), max(0,1), max(0,0)] = [2, 1, 0]
// then P2 increments its own slot
P2_clock = [2, 2, 0]

With this structure, you can compare two vectors and get a real answer to "did A cause B, did B cause A, or are they concurrent." If every component of vector A is less than or equal to the matching component of vector B, A happened-before B. If neither vector dominates the other — some components higher in A, others higher in B — the two events are genuinely concurrent: neither could have influenced the other, because neither process had seen the other's event yet.

Why "concurrent" is the useful answer, not a failure

This is exactly the situation leaderless, multi-master data stores hit constantly: two clients update the same record on two different replicas before either replica has heard from the other. A vector clock comparison tells the system, correctly, that neither update happened-before the other — they're a real conflict, not a case where one is simply stale and should be discarded. Some databases historically used this exact mechanism (Amazon's Dynamo being the best-known example) to detect these true conflicts and hand them back to the application, or to a client-side merge function, rather than silently picking a winner based on an untrustworthy wall-clock timestamp and quietly losing one of the two updates.

The cost that keeps vector clocks from being universal

The vector grows by one slot for every participant that can generate events, and every message needs to carry the full vector. In a system with thousands of clients, that's an impractical amount of metadata attached to every single message. This is why vector clocks tend to appear between a bounded, small number of server-side replicas rather than being extended out to every client directly — and why many practical systems settle for a cheaper approximation, like last-write-wins by wall-clock timestamp, accepting the occasional lost update in exchange for not carrying a growing vector on every message.