Clocks, Causality, And Why HLC
Distributed systems need some way to talk about event order. But different clock models answer different questions, and they do not all provide the same kind of truth.
Clocks, Causality, And Why HLC
This guide explains where causal-order sits in that landscape:
- why Lamport clocks are useful but incomplete
- why vector clocks are richer but heavier
- why this library uses Hybrid Logical Clocks
- what the HLC clock helper in
causal-orderactually does - why that helper is not Cristian's algorithm
The Short Version
causal-order does not treat any clock as a complete causality oracle.
Its practical stance is:
- explicit causal metadata is strongest
- same-node monotonic sequence is strong local evidence
- HLC is useful cross-node ordering metadata
- HLC alone is still only
derived, notproven
That is why the library combines HLC with parent links, dependency links, and same-node sequence evidence instead of pretending one clock solves the whole problem.
This is also one of the practical consequences of distributed-systems impossibility results: you do not get one perfect global timeline for free, so the library has to distinguish useful ordering from provable causality.
Lamport Clocks
Lamport clocks give you a logical ordering discipline:
- if one event causally happened before another and the protocol propagates Lamport timestamps correctly, the earlier event gets the smaller timestamp
- they are cheap and simple
- they capture causal precedence better than naive wall-clock timestamps
But Lamport clocks do not tell you:
- whether two events are concurrent
- how far apart events were in physical time
- whether two close timestamps mean anything about real elapsed time
That matters because many operational systems need more than "some logical order." They also need a time-aware shape for replay, audit, and windowed stream handling.
Vector Clocks
Vector clocks carry richer causal information than Lamport clocks.
They can tell you:
- one event happened before another
- two events are concurrent
That is powerful. But it comes with real cost:
- metadata grows with the participant set
- dynamic membership makes the vectors harder to manage
- serialization, storage, and comparison overhead all rise
- many event pipelines do not naturally preserve vector-clock state end to end
For some systems, that tradeoff is worth it. For many practical event pipelines, it is too heavy for the default event envelope.
Why This Library Uses HLC
Hybrid Logical Clocks are a practical middle ground.
They preserve:
- a physical-time component
- a logical counter for monotonic progress when wall time stalls or messages arrive out of order
That gives the library something very useful:
- time-aware ordering that is more operationally honest than raw wall-clock sort
- monotonic same-node clock progression without pretending clocks are perfectly synced
- a reasonable event-time field for batch ordering and stream watermark handling
But HLC still does not prove causality by itself.
If:
A.hlc < B.hlc
that can justify a useful derived ordering.
It does not prove that B observed, depended on, or was caused by A.
That is exactly why causal-order treats HLC-only ordering as derived.
What The HLC Clock Helper Does
causal-order exposes createHlcClock() with three practical inputs:
nodeId- optional
now()source - optional
maxDriftMs
That clock then exposes three operations:
now()receive(remote)getState()
now()
now() advances local HLC state using the local wall-clock source.
In practice:
- if wall time moved forward,
physicalTimeMsadvances andlogicalCounterresets to0 - if wall time did not move forward, the clock keeps the same physical time and increments
logicalCounter
That preserves monotonic local progress even when wall time stalls or moves too slowly to distinguish nearby events.
receive(remote)
receive(remote) merges a remote HLC timestamp into local state.
In practice:
- the remote timestamp is validated
- local wall time is read
- if
maxDriftMsis configured and the remote physical time is too far in the future, the merge is rejected - the new physical time becomes the maximum of:
- local HLC physical time
- remote HLC physical time
- local wall time
- the new logical counter is chosen according to the standard HLC merge cases
That gives the node a monotonic merged timestamp without pretending either side had perfect wall-clock truth.
getState()
getState() returns a copy of the current HLC state.
That is mostly an operational convenience for inspection, serialization, and testing.
What The HLC Clock Helper Does Not Do
It is just as important to say what this code does not do.
It does not:
- synchronize clocks across machines
- estimate network delay
- calculate a global canonical time
- prove causality from clock values alone
- provide vector-clock-style concurrency detection
So when the library uses HLC, it is using a disciplined event timestamp format. It is not claiming distributed time has been solved.
Why This Is Not Cristian's Algorithm
Cristian's algorithm is a clock-synchronization protocol.
Its purpose is to let a client estimate a better wall-clock time by talking to a time server and accounting for round-trip delay.
That is not what causal-order's HLC clock helper does.
The HLC helper:
- does not contact a time server
- does not estimate one-way or round-trip network delay
- does not compute clock offset from request-response exchange
- does not align nodes to one server-derived wall-clock value
Instead, it assumes each node already has some local wall-clock source and then builds a monotonic hybrid logical timestamp on top of that local source plus remote event timestamps.
So the honest comparison is:
- Cristian's algorithm tries to improve wall-clock synchronization
- the HLC helper in
causal-ordertries to preserve monotonic event timestamps despite imperfect clocks and message timing
Those are related concerns, but they are not the same mechanism.
Where Explicit Causality Still Matters
This is the key architectural point for causal-order.
Even with HLC, explicit causality still matters because the library wants to distinguish:
- "these events look earlier and later"
- from
- "this event actually depends on that one"
That is why parent links, dependency links, and same-node monotonic sequence still matter so much.
In current releases:
- explicit parent and dependency links can produce
provencausal ordering - same-node monotonic sequence can produce strong same-node ordering evidence
- HLC order across nodes is useful, but remains
derived
That separation is intentional. It keeps the library operationally useful without letting clock metadata impersonate causal proof.
Why Not Use Vector Clocks Instead
If your system can afford vector-clock metadata and preserve it end to end, vector clocks may let you express concurrency more directly than HLC.
But this library is aimed at a more common operational shape:
- event records already have timestamps or HLC-like fields
- metadata budget matters
- event envelopes cross many system boundaries
- explicit dependency edges are available only in some flows
For that shape, HLC plus explicit causal metadata is often a more deployable compromise than requiring full vector-clock propagation everywhere.
Practical Takeaway
If you want the shortest honest summary, it is this:
- Lamport clocks are simpler but less time-aware
- vector clocks are richer for causality but heavier operationally
- HLC is a practical middle ground for event ordering
causal-orderstill needs explicit causal metadata because HLC alone is not proof
That is the model this library is built around.