Why 'Last Write Wins' Is Secretly Deleting Your Agents' Work
Day 9: Vector Clocks — When 'Happened Before' Is a Lie
Ask a Lamport clock which of two events happened first, and it will always answer — that's exactly what should worry you. Feed it two writes that never crossed paths, and it still hands you a confident 'A before B.' Trust that answer in production and you don't get a bug report. You get a customer's data quietly vanishing.
Cold open: the vanishing write
Two nodes, one key. Node 1 writes A, stamps it Lamport 5. Node 2 has never heard of A — it writes B, stamps it 6. Later, a merge process looks at both timestamps, sees 5 < 6, and reasons the way it was built to reason: B happened after A, so B wins, A gets discarded. Except nothing happened after anything. A and B never interacted. They were two independent writes that landed on the same key around the same time — genuinely concurrent, no causal link between them at all. The clock didn't spot a conflict. It invented a winner. Nobody finds out until a customer opens their cart, or their profile, or their document, and it's somehow reverted to an old version.
Callback to Day 8: total order was always fake
Day 8 sold you on Lamport clocks with a clean promise: two integers always compare, so you can always sort your events, no exceptions. That's true — and it's also the whole problem. The comparability isn't something you discovered in the events; you manufactured it, with tie-breaking rules (bump the counter on send and receive, break anything left over with node ID). Real causality isn't like that. It has actual gaps in it: plenty of event pairs genuinely have no before-or-after relationship, full stop. A scalar clock has no way to say that. Its vocabulary is 'before' and 'after' — nothing else — so when the true answer is 'neither,' it picks one anyway and says it with a straight face.
The core mechanism: one counter per node
The fix: stop using one counter, use one per node. A vector clock is an array with a slot for every node in the system. Node i touches only its own slot on a local event, and when a message arrives, it merges in the max of every slot it's told about. The comparison rule changes too — no more lexicographic shortcut. V1 happens-before V2 only if every component of V1 is ≤ the matching component of V2, with at least one strictly less. And if neither vector fully dominates the other — some slots favor one, some favor the other — that's not a case you patch around. That's the system telling you the truth: these two events are concurrent, and no amount of squinting produces an order that doesn't exist.
The same scenario, with vector clocks
Rerun the cold open with two-slot vectors, [Node1, Node2]. Node 1 writes A locally: its vector becomes [1, 0]. Node 2, off doing its own thing, writes B: its vector becomes [0, 1]. Now compare. Is [1,0] ≤ [0,1] everywhere? No — the first slot already fails. Is [0,1] ≤ [1,0] everywhere? No — the second slot fails this time. Neither one dominates. The system doesn't guess, doesn't pick a side — it knows, with certainty, that A and B are concurrent. Not that one beat the other. That the question 'which came first' doesn't have an answer.
So what does a correct system do with 'concurrent'? Not guess. It keeps both values as siblings and pushes the conflict to whoever can actually make the call — the application, or the person, with something like 'these two carts diverged, merge them.' This is the Dynamo playbook: the clock's job stops at detection. Resolution is somebody else's problem, on purpose.
The one rule worth memorizing
- ▹A happens-before B iff A ≤ B in every component AND A < B in at least one component
- ▹Otherwise, A and B are concurrent — write it as A || B
- ▹Component-wise comparison, never a single scalar comparison — that's the entire difference from Lamport clocks
Cost and shape
Every event now drags along O(n) worth of metadata, where n is the number of nodes that can generate writes — one integer per replica instead of one integer, period. At 3-5 replicas, which is roughly what a typical distributed datastore's replication group looks like, that's nothing: a handful of bytes tacked onto each record. It stops being nothing the moment n grows — lots of clients, lots of sessions, lots of independent writers hitting the same key — because now every record's metadata grows with every writer that has ever touched it, forever. That's exactly the problem tomorrow's lesson exists to fix: version vectors and dotted version vectors keep the same concurrency-detection guarantee without making you pay O(n) forever.
Why this matters more, not less, in the AI era
This stopped being a database-internals footnote the day agent systems started writing to shared state concurrently. Picture a multi-agent pipeline: one agent updating a shared plan, another writing tool results back, a third patching the same memory store — that's structurally the same problem as two Dynamo nodes hitting the same key. If your orchestration layer resolves conflicting writes with 'last write wins' by timestamp — which is just a Lamport-style total order wearing a different hat — you get the exact same failure. One agent's tool result, or plan update, quietly disappears, and the rest of the system keeps going on stale state, confident nothing's wrong. It's also why any framework that fans out to parallel sub-agents and then merges their output needs a real answer to 'two sub-agents just wrote conflicting updates to the same slice of state — now what.' Vector-clock-style concurrency detection, or its modern descendants like CRDTs, is the honest answer. Picking a winner by timestamp is the trap dressed up as a solution.
Where you can see this in production
Riak built on vector clocks directly — when it detected concurrency, it handed the client both sibling values and let the application deal with it. Amazon's original Dynamo paper is still the primary source for the exact design walked through above. And Cassandra's 'last write wins' default is the cautionary tale worth reading precisely because it's honest about the trade: skip the vector-clock overhead, and in exchange accept silent overwrites as a feature of the system, not a bug in it.
Extend your knowledge
- ▹Read the original Amazon Dynamo paper's section on vector clocks and sibling resolution — it's the primary source for the design in this lesson
- ▹Look up how Riak exposed vector clock conflicts to clients (siblings API) to see concurrency detection surfaced at the application layer
- ▹Compare Cassandra's last-write-wins default against Riak's sibling approach to see the concrete tradeoff between simplicity and silent data loss
- ▹If you work with multi-agent orchestration frameworks, check how they merge concurrent writes to shared state/memory — ask whether it's timestamp-based (Lamport-style) or conflict-aware (vector-clock-style)
Discussion
Chat with Chi Cong (AI) about this article. Your conversation is private to you — you can publish a summary for others when you're done.