Hybrid Logical Clock
A Hybrid Logical Clock (HLC) combines a physical wall-clock timestamp with a logical counter to achieve causality tracking that stays close to real time. Based on the Kulkarni et al. 2014 paper, HLC provides O(1) causality tracking with configurable drift enforcement — making it ideal for high-throughput distributed systems where wall-clock proximity matters.
Key Types
| Type | Description |
|---|---|
HlcTimestamp | Immutable (wallTime, counter, nodeId) tuple with total ordering |
HlcGuidFactory | Generates UUIDv7 Guid values with HLC semantics; also exposes/witnesses HlcTimestamp state |
HlcCoordinator | Manages BeforeSend / BeforeReceive coordination for a node |
HlcMessageHeader | Wire format for propagating an HlcTimestamp across service boundaries |
HlcStatistics | Counters for send/receive operations and drift observations |
HlcClusterRegistry | Manages a set of HlcGuidFactory nodes for simple cluster simulations |
Timestamp Structure
An HlcTimestamp is a (wallTime, counter, nodeId) tuple that defines a total order:
- Compare
wallTime(higher = later) - If equal, compare
counter(higher = later) - If equal, compare
nodeId(higher = later)
This total order means every timestamp is unambiguously before or after every other timestamp.
Encoding Formats
64-bit packed (ToPackedInt64 / FromPackedInt64)
An optimized single-long encoding suitable for compact storage:
- 48 bits for wall time (milliseconds)
- 12 bits for logical counter
- 4 bits for node ID (node ID is truncated — only the lowest 4 bits are preserved)
Use this when you need compact storage and don't need full node ID fidelity.
80-bit canonical (WriteTo / ReadFrom)
A full-fidelity 10-byte big-endian encoding:
- 48 bits for wall time
- 16 bits for counter
- 16 bits for node ID
Use this for wire formats and persistent storage where full fidelity is required.
BeforeSend / BeforeReceive
The HlcCoordinator implements the two-operation protocol for causality propagation:
var tp = new SimulatedTimeProvider();
using var aFactory = new HlcGuidFactory(tp, nodeId: 1);
using var bFactory = new HlcGuidFactory(tp, nodeId: 2);
var a = new HlcCoordinator(aFactory);
var b = new HlcCoordinator(bFactory);
// A sends a message
var t1 = a.BeforeSend();
var header = new HlcMessageHeader(t1, correlationId: Guid.NewGuid());
var headerValue = header.ToString(); // e.g. "1700000000000.0000@1;d7c3... (N format)"
// B receives the message — witnesses A's timestamp and advances its own
var parsed = HlcMessageHeader.Parse(headerValue);
b.BeforeReceive(parsed.Timestamp);
// B sends a reply — guaranteed to be causally after the received timestamp
var t2 = b.BeforeSend();
Console.WriteLine(t1 < t2); // trueBeforeReceive witnesses the full remote HlcTimestamp including its node ID for tie-breaking.
HLC GUIDs (UUIDv7 encoding)
HlcGuidFactory also generates UUIDv7 Guid values that embed the HLC wall time and counter (and a node id field).
using var factory = new HlcGuidFactory(TimeProvider.System, nodeId: 42);
var (guid, hlc) = factory.NewGuidWithHlc();
Console.WriteLine(guid);
Console.WriteLine(hlc); // "walltime.counter@node"You can reconstruct an HlcTimestamp from a GUID produced by HlcGuidFactory:
var decoded = guid.ToHlcTimestamp();
Console.WriteLine(decoded?.WallTimeMs);
Console.WriteLine(decoded?.Counter);
Console.WriteLine(decoded?.NodeId); // node id is stored in 14 bits in the UUIDDrift Configuration
HlcOptions controls how the coordinator handles clock drift:
var tp = TimeProvider.System;
// Strict: throw HlcDriftException when drift exceeds the bound
using var strict = new HlcGuidFactory(tp, nodeId: 1, options: new HlcOptions
{
MaxDriftMs = 1_000,
ThrowOnExcessiveDrift = true
});
// High-throughput: silently allow drift beyond the bound (maintains monotonicity)
using var highThroughput = new HlcGuidFactory(tp, nodeId: 1, options: new HlcOptions
{
MaxDriftMs = 60_000,
ThrowOnExcessiveDrift = false
});Clockworks also provides a few ready-made presets:
HlcOptions.StrictHlcOptions.DefaultHlcOptions.HighThroughput
Trade-offs
| Property | HLC |
|---|---|
| Space complexity | ✅ O(1) — single timestamp regardless of cluster size |
| Time complexity | ✅ O(1) — send and receive are constant time |
| Wall-clock proximity | ✅ Bounded drift from physical time |
| Causality tracking | ✅ Preserved via happens-before ordering |
| Concurrency detection | ❌ Cannot detect concurrent events |
| Physical clock dependency | ❌ Requires reasonably synchronized clocks |
See HLC vs Vector Clocks for a full comparison and decision guide.
Running the Demos
# Propagating HLC across service boundaries (header format)
dotnet run --project demo/Clockworks.Demo -- hlc-messaging
# BeforeSend/BeforeReceive workflow with coordinator statistics
dotnet run --project demo/Clockworks.Demo -- hlc-coordinator