The question this answers
If I cannot use clocks, what does it even mean for one event to come before another?
The happens-before relation a → b is a partial order over events. It guarantees that if a → b then a could have influenced b, and if neither a → b nor b → a then neither could have influenced the other — they are *concurrent*. It guarantees nothing about physical time: a → b does not mean a occurred earlier by any clock, and two concurrent events may be hours apart.
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.
A node knows the order of its own events (it executed them in that order) and knows that every message it received was sent before it was received. From those two local facts alone it can derive a large part of the global causal order — without any clock, and without any coordination. What it cannot derive locally is whether an event it has never heard about is concurrent with one of its own; that requires the metadata of Vector Clocks: Buying Concurrency Detection at O(N).
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.
Three rules, and everything follows
Lamport's definition is famously small. a → b ("a happens before b") is the smallest relation satisfying three rules:
That is the whole definition. Everything in this module — Lamport clocks, vector clocks, causal consistency, version vectors — is machinery for tracking or approximating this relation, so it is worth being exact about what it says.
Crucially, → is not a total order. Take two events on different nodes with no chain of messages connecting them: neither happens before the other. They are concurrent, written a ∥ b. Concurrency here does not mean "at the same time" — two concurrent events can be a week apart. It means *neither could have known about the other*, which is the property that actually matters when you are deciding whether they conflict.
- Program order. If
aandbhappen on the same node andacomes first, thena → b. - Message order. If
ais the sending of a message andbis its receipt, thena → b. - Transitivity. If
a → bandb → c, thena → c. - Concurrency is the default. Anything not related by those rules is concurrent — the relation is a partial order, not a total one.
The same relation you already met in Concurrency — at a different scale
If you have read the Concurrency domain, this will feel familiar, and it should: it is literally the same relation. Concurrency's happens-before is the edge that makes one thread's write visible to another — established by a lock release paired with an acquire, or by a release-store paired with an acquire-load. Ours is established by a message send paired with a receive. Same relation, same partial order, same consequence: anything not ordered is concurrent, and concurrent access to the same datum is where the bugs live.
The two differ only in what plays the role of the synchronising edge, and in what happens when you get it wrong. Inside a machine, the edge is provided by hardware and compiler mechanisms — the memory model defines which pairs are ordered, and violating it gives you a torn read or a stale value. Across machines, the edge is provided by an actual message, the ordering is not enforced by anything, and violating it gives you a conflicting write that nobody detects until a merge.
The naming here is deliberate: this domain calls it causal-ordering so that it can never be confused with the memory-model edge, which the Concurrency domain owns. Read them together. The transferable insight is that "concurrent" is a *structural* property — the absence of a synchronising edge — and not a statement about time. Once that clicks in one domain it is free in the other.
| Inside a machine (Concurrency) | Across machines (here) | |
|---|---|---|
| What creates the edge | Lock release/acquire, release-store/acquire-load, thread start/join | Message send/receive, and program order on a node |
| Who enforces itprotocol | Compiler and CPU, per the memory model | Nobody. The application must track it if it wants it. |
| Cost of the edgetypical | A barrier: tens to hundreds of cycles | A network round trip: microseconds to hundreds of milliseconds |
| Symptom when missing | Stale read, torn value, a race that reproduces once a month | Conflicting writes, lost update, divergent replicas |
| Detectionassumption | Race detectors; the schedule is enumerable in principle | Version metadata; concurrency is detectable only if you carried it |
Why a partial order is the right answer, not a weaker one
The instinct on first meeting → is that it is deficient: it fails to order some pairs, so surely a better mechanism would order all of them. That instinct is exactly backwards, and getting past it is the point of this lesson.
A total order over all events is *available* — you can always impose one (Total Order Broadcast Is Consensus Wearing a Different Hat) — but it is expensive, because it requires the nodes to agree, which requires consensus, which requires round trips and a majority to be reachable. The partial order is free: every node can derive it from information it already has, with no messages beyond the ones the application was already sending.
More importantly, a total order manufactures information that does not exist. If two users edit different fields of a document with no knowledge of each other, there is no fact of the matter about which came first. Imposing an order does not discover the truth; it invents one, and inventing one is how Last Write Wins Is Data Loss You Chose by Default silently discards a real edit. The partial order preserves the useful distinction: *ordered* pairs have a right answer, *concurrent* pairs need a merge decision from the application (Only the Application Knows What the Merge Means).
So the shape of a good design is: track causality, let the partial order do the work it can do for free, and reserve coordination for the specific places where you genuinely need concurrent events to be ordered — usually because an invariant spans them (Start From the Invariant, Not From the Architecture).
What causality buys you, concretely
Causal ordering is the strongest consistency model that can be provided without giving up availability during a partition — a result worth remembering, because it puts a precise ceiling on what "available" systems can offer (Causal Consistency: Never Show an Effect Before Its Cause, CAP: What the Theorem Actually Says).
Concretely, it is what makes these scenarios behave sanely: a reply never appears before the comment it replies to; a photo never shows up in a feed before the album that contains it; after you remove someone from an access list and then post, they do not see the post. Each of those is a causal dependency created by one user's actions passing through the system, and each is a real bug when the dependency is dropped.
What it does *not* buy: any statement about events that are genuinely concurrent. If two people simultaneously edit the same field, causality tells you truthfully that they are concurrent and hands the problem back to you. That is not a failure of the model. It is the model correctly reporting that your application, not the ordering mechanism, has to decide.
Alice: removes Bob from "close friends" (event a) Alice: posts "close friends only" photo (event b) a -> b Bob: sees the photo Cause: the two writes went to different replicas, and b propagated to Bob's replica before a did. Without causal delivery there is nothing in the system that knows b depended on a. The fix is not "make replication faster". It is to carry the dependency with the write, and hold b until a is applied.
Key points
- Happens-before is defined by three rules: program order, message send-before-receive, and transitivity.
- It is a partial order. Events not related by it are *concurrent* — meaning neither could have influenced the other, not that they occurred simultaneously.
- It is the same relation the Concurrency domain calls
happens-before; only the synchronising edge differs (a message rather than a lock or a barrier). - The partial order is free — derivable locally. A total order costs consensus.
- A total order over concurrent events invents information that does not exist, which is how silent data loss enters a system.
- Causal consistency is the strongest model available without sacrificing availability under partition.
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.
- • Each node records its own events in the order it executed them; this establishes program order locally with no coordination.
- • When a node sends a message it attaches whatever causal metadata the scheme requires (a counter, a vector, a set of dependencies).
- • On receipt, the receiver merges that metadata into its own state, establishing the send → receive edge.
- • Transitivity then propagates: the receiver's subsequent events inherit the whole causal past of the message it received.
- • Two events are declared concurrent exactly when neither node's metadata dominates the other's.
- • A message is lost, so an edge that should exist never forms and two causally related events look concurrent.
- • Causal metadata is stripped at a boundary — a queue, a proxy, a serialisation format that drops unknown fields — silently converting a causal chain into unrelated events.
- • Causality flows *outside* the system: two services communicate through a user's browser, a phone call, or a shared spreadsheet, and no message inside the system records the dependency.
- • A node restarts and loses its causal state, so its subsequent events appear unrelated to its own past.
- • Metadata grows without bound and someone truncates it as an optimisation, breaking the relation it encoded.
- • The reply-before-comment bug: a user sees a response to something that is not there yet. The operator sees a support ticket describing an impossible UI state, and both writes look correct in every log.
- • Privacy violation via reordering: an access-control change and a subsequent post are applied in the wrong order on a replica, and someone sees content they were just removed from. Nothing errors; the audit log shows both operations succeeding.
- • Hidden causality through a side channel: two services are actually dependent because a human copied a value between them, so the system sees concurrent writes and merges them, dropping one. The operator cannot reproduce it because the dependency is not in any trace.
- • Metadata stripped by an intermediary: after introducing a message broker, previously-ordered writes start being reported as concurrent. The operator observes a step change in conflict rate at exactly the deploy time of the broker.
- • Causal delivery stalls: a node holds a message waiting for a dependency that was lost, and that key silently stops updating on that replica while everything else works.
- • Deriving the partial order requires no coordination at all — that is its defining economic advantage, and it is why causal consistency is available under partition.
- • The cost is metadata carried on messages, which is bandwidth and storage rather than round trips and availability.
- • Turning the partial order into a total order requires agreement among nodes: see Four Orderings, Four Prices and Total Order Broadcast Is Consensus Wearing a Different Hat for what that costs.
- • The relation itself is unaffected by node failure or partition — it is defined over the messages that did happen, not over the ones that should have.
- • During a partition, each side continues to extend the order locally and correctly; events across the partition are concurrent, which is a true statement.
- • After healing, the two histories merge into a single partial order with a large set of concurrent pairs — exactly the set the conflict module has to resolve.
- • Detect: measure the rate of concurrent-pair detections. A sudden change usually means metadata is being lost, not that user behaviour changed.
- • Contain: buffer messages whose causal dependencies have not arrived, rather than applying them out of order — with a bounded buffer and an explicit policy when it fills.
- • Recover: request missing dependencies explicitly from a peer, or fall back to a full state sync when the gap is too large (Anti-Entropy: Repairing Divergence Nobody Reported).
- • Reconcile: resolve the accumulated concurrent pairs by application rule, not by timestamp (Only the Application Knows What the Merge Means).
- • Verify: replay a known causal chain end to end and assert the effects appear in dependency order at every replica.
- • Rate of pairs classified as concurrent versus causally ordered — a direct measure of how much real conflict your workload generates.
- • Size of the pending-dependency buffer per replica; growth means an edge is missing and something is stuck.
- • Time from an event's creation to its causal dependencies being satisfied at each replica — the practical latency of causal delivery.
- • Count of messages arriving with missing or unparseable causal metadata, broken down by ingress path, which is how you find the intermediary that strips it.
- • Anywhere a user's actions create dependencies that must be visible in order: social feeds, comment threads, access-control changes followed by content, multi-step workflows.
- • As the correctness frame for any system that must stay available under partition, since causal is the ceiling there.
- • As a design tool: asking "is this pair genuinely ordered, or am I inventing an order?" is the fastest way to find a lurking lost-update bug.
- • Where a single node already sequences everything, tracking causality adds metadata and buys nothing — the node's program order is already a total order.
- • For workloads where every write is to a distinct key with no cross-key dependency, the machinery is pure overhead.
- • Where the business genuinely requires a global total order (a ledger, a sequence of trades), causality alone is insufficient and you must pay for consensus.
- • Impose a total order with a single sequencer or consensus, and stop reasoning about concurrency at all — simpler to think about, and much more expensive to run: Total Order Broadcast Is Consensus Wearing a Different Hat.
- • Partition so that causally related writes always land on the same node, making local program order sufficient: Hash Partitioning and the Modulo Trap.
- • Accept eventual consistency with no causal guarantee and handle anomalies in the UI, which is a legitimate choice for low-stakes data: Eventual Consistency: If Updates Stop, Replicas Converge.
- • Track causality only for the specific keys or entities where it matters, rather than globally — dependency tracking scoped to a document, a conversation, a cart.
Happens-before: three rules, and everything follows
| # | Node | Event | Pick |
|---|---|---|---|
| 1 | P | write x = 1 | a |
| 2 | P | send → Q: x = 1 | |
| 3 | Q | unrelated work | |
| 4 | Q | receive | |
| 5 | Q | send → R: derived y | |
| 6 | R | write z (nobody told it anything) | b |
| 7 | R | receive | |
| 8 | P | write x = 2 |
What people believe, and what is true
Concurrent means "at the same time".
It means neither event could have influenced the other. Two concurrent events can be a week apart, and two events a nanosecond apart can be causally ordered.
happens-before is about time.
It is about *potential influence*. It deliberately says nothing about physical time, which is exactly what makes it usable when clocks are untrustworthy.
A partial order is a weaker version of a total order.
It is a more honest one. It orders every pair for which an ordering exists, and refuses to invent one for the rest. Inventing is what loses data.
This is different from the happens-before in the Concurrency domain.
It is the same relation. The synchronising edge is a message instead of a lock or barrier, and nothing enforces it for you.
Go deeper
Only the levels this lesson can honestly fill — a missing level is a claim nobody had.
Overview
One event happens before another if it could have influenced it — through program order on a node, or through a message. Everything else is concurrent, meaning neither knew about the other.
Practical
Design by asking which pairs of operations are genuinely ordered. Carry causal metadata on writes so the system can tell ordered from concurrent, and route concurrent pairs to a real merge rule instead of a timestamp comparison. Watch for intermediaries that strip metadata.
Advanced
The relation is the reflexive-transitive closure of program order and message order, and the resulting structure is a directed acyclic graph of events. Causal consistency is the strongest model implementable in an always-available system, which is why it is the practical ceiling for AP designs. Beyond it you are paying for agreement.
Apply it
- 🔧 Draw the spacetime diagram for a three-node interaction in your own system and list every concurrent pair.
- 🔧 Find a place where your code compares timestamps to decide precedence, and determine whether the two events are ever genuinely concurrent. If they are, that comparison is losing data.
- ⚡ A user removes a follower and immediately posts. The removed follower sees the post. Both operations succeeded. Explain the mechanism and the fix.
- ⚡ Two microservices exchange no messages, but a human operator copies an id from one UI to the other. Does the system see a causal dependency? What are the consequences?
- 💬 Define happens-before. Then give two events that are concurrent despite occurring an hour apart.
- 💬 Why is causal consistency the strongest model available to a system that must stay up during a partition?
- 💬 Your system reports a sudden increase in concurrent writes after a deploy that added a message broker. What is your first hypothesis?