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:
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.
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.
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 DetectedSVG
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.