Concept · Distributed Systems

Vector Clocks & LWW

01

Why this matters

Two users edit the same document at (roughly) the same moment. Their clients both commit locally. Now the server has two versions. Which one wins?

"The one with the later timestamp" sounds fine — until you realize clocks drift, NTP skews by 100ms, and two events with "different timestamps" might actually have been simultaneous. Worse: two events with the "same timestamp" might have a causal relationship you need to preserve.

Vector clocks and Last-Write-Wins are the two opposite answers. Each is right somewhere.

02

Wall-clock time is a trap

Three ways wall clocks betray you in distributed systems:

  1. Drift. Server A's clock runs 50ms fast. Server B's is 30ms slow. They can disagree about event order by 80ms even when NTP is working.
  2. Jumps. A server's clock can leap backward (NTP correction). An event timestamped at 12:00:00.500 can be "followed" by one at 12:00:00.400.
  3. Leap seconds. On leap-second days, one second is repeated. Duplicates of "same timestamp" pile up.

These make timestamps unreliable for ordering. Logical clocks (vector clocks, Lamport timestamps) don't depend on wall time — they track causality.

03

Vector clocks

Each node keeps a vector — one counter per node in the system. [A:0, B:0, C:0] initially.

  • Local event. Node A does something → increments its own counter: [A:1, B:0, C:0].
  • Send a message. Include your current vector.
  • Receive a message. Take element-wise max of your vector and theirs, then increment your own counter.

Given two vectors V1 and V2, you can determine their causal relationship:

  • V1 ≤ V2 (every element V1[i] ≤ V2[i]) AND V1 ≠ V2 → V1 happened-before V2.
  • Neither V1 ≤ V2 nor V2 ≤ V1 → the events are concurrent (conflict!). Neither caused the other.

This lets the system detect conflicts reliably. It doesn't resolve them — that's the application's job.

Vector clock merge (causality tracking)
def merge(a, b):
    """Pointwise max: a[node] = max(a[node], b[node])"""
    out = dict(a)
    for node, ts in b.items():
        out[node] = max(out.get(node, 0), ts)
    return out

def happens_before(a, b):
    """True if a ⇒ b (a causally precedes b)"""
    strict = False
    for node in a.keys() | b.keys():
        av, bv = a.get(node, 0), b.get(node, 0)
        if av > bv: return False
        if av < bv: strict = True
    return strict

def concurrent(a, b):
    return not happens_before(a, b) and not happens_before(b, a)

# Example: A=[A1,B0], B=[A0,B1] → concurrent (neither precedes).
# On read, server returns both versions; app resolves.
Vector Clocks — Concurrent Edits Detected SVG
A B C [1,0,0] [2,0,0] [1,1,0] [2,2,0] [1,0,1] B's [2,2,0] vs C's [1,0,1] neither ≤ the other → CONCURRENT A,B share a causal chain · C's edit is independent
04

LWW — the simple compromise

Vector clocks

Detects conflicts precisely

You know when two writes conflict. You can preserve both, present them to users ("pick a version"), or merge them intelligently. Costs: vectors grow with the number of writers. Pruning is its own problem.

Last-Write-Wins (LWW)

Pick by timestamp, ignore conflicts

Each write has a timestamp; latest wins on read. Simple. Works when conflicts are rare OR when losing one side is acceptable. Costs: silently discards concurrent writes. A user's edit can vanish.

When LWW is fine

User settings (last change wins). Session state (last interaction wins). Timestamps as heuristics.

When LWW is catastrophic

Shopping cart (two adds = one item lost). Collaborative editing (two concurrent edits = one lost). Counters (two +1 = +1). Any aggregating operation.

05

Deep dive — CRDTs, the modern answer

Conflict-free Replicated Data Types (CRDTs) give you LWW's simplicity with vector-clock-level correctness — for specific data types.

G-Counter (grow-only counter): each node has a private counter; the "real" value is the sum of all. Concurrent increments from A and B both count. No conflict possible.

PN-Counter (positive-negative counter): two G-Counters, one for increments, one for decrements. Real value = inc − dec.

OR-Set (observed-remove set): each add gets a unique token; remove deletes the tokens you've observed. Concurrent add + remove leaves the item present (the add's token wasn't in the remove's observed set).

RGA / LSEQ (sequence types, like text documents): concurrent inserts produce deterministic orderings so two clients converge to the same string without coordination.

Google Docs' Operational Transformation is a cousin of CRDTs. Figma uses CRDTs directly. Redis has CRDT data types (Redis Enterprise Active-Active). If your workload fits a CRDT, you get conflict-free merges without vector clocks or LWW — the best of both.

06

Real-world

DynamoDB / Cassandra

LWW by default

Write timestamps set by client (or coordinator). Latest wins. Users have been burned — lost writes — and now use client-side version vectors for critical data.

Riak

Vector clocks surfaced to the app

Reads return all concurrent siblings; application picks a winner or merges. More correct but higher complexity.

Figma / Linear / Notion

CRDTs

Multi-user collaborative editing without a central lock. Each user's operations merge deterministically into the shared state.

Redis Active-Active

CRDT-backed global replication

Counters, sets, hashes, sorted sets all have CRDT implementations. Writes merge without conflict across regions.

07

Used in problems

Google Docs uses Operational Transformation (CRDT-adjacent). Google Drive uses vector clocks to merge folder changes across offline devices. Distributed logging uses LWW for log rotation metadata.

Next up