Loading content...
When multiple writers update the same data concurrently, one of them must prevail. Last-Write-Wins (LWW) offers the simplest answer: whichever write has the most recent timestamp wins. The losing write is discarded entirely.
This approach is seductively simple. There's no need for complex merge logic, no version tracking beyond timestamps, and resolution is deterministic—given the same conflicting writes, all replicas will independently arrive at the same answer. This simplicity has made LWW the default conflict resolution strategy in many distributed databases, from Cassandra to DynamoDB to CouchDB.
LWW's elegance masks a fundamental trade-off: it guarantees data loss when real conflicts occur. If two users genuinely update the same record concurrently, one complete update vanishes without trace. As we'll see, this is acceptable in some contexts and catastrophic in others.
By the end of this page, you will fully understand the LWW mechanism, the critical role of timestamps, clock synchronization challenges, the precise trade-offs involved, and the scenarios where LWW is the right choice—and where it's dangerously inadequate.
Last-Write-Wins operates on a straightforward principle: every write carries a timestamp, and when multiple writes conflict, the write with the highest (most recent) timestamp is kept while others are discarded.
The Algorithm:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758
interface LWWValue<T> { value: T; timestamp: number; // Typically Unix epoch milliseconds} class LWWRegister<T> { private state: LWWValue<T> | null = null; /** * Write a new value with timestamp. * Returns true if the write was accepted. */ write(value: T, timestamp: number): boolean { if (this.state === null || timestamp > this.state.timestamp) { this.state = { value, timestamp }; return true; // Write accepted } return false; // Write rejected (older timestamp) } /** * Read the current value. */ read(): T | null { return this.state?.value ?? null; } /** * Merge with another LWW register (for replication). * This is the conflict resolution: higher timestamp wins. */ merge(other: LWWValue<T>): void { if (this.state === null || other.timestamp > this.state.timestamp) { this.state = other; } // If other.timestamp <= this.state.timestamp, keep current } getState(): LWWValue<T> | null { return this.state; }} // Example usage:const register = new LWWRegister<number>(); register.write(100, 1000); // Initial write at timestamp 1000console.log(register.read()); // 100 register.write(150, 1500); // Newer write at timestamp 1500console.log(register.read()); // 150 register.write(125, 1200); // Older write at timestamp 1200console.log(register.read()); // Still 150 (older write rejected) // Merging from another replica:register.merge({ value: 200, timestamp: 1800 });console.log(register.read()); // 200 (replica's value had higher timestamp)The LWW Register is technically a Conflict-free Replicated Data Type (CRDT). Its merge operation is commutative (order doesn't matter), associative (grouping doesn't matter), and idempotent (merging the same value twice has no additional effect). This means replicas will converge to the same state regardless of the order they receive updates.
LWW depends entirely on timestamp ordering. If timestamps are wrong, the 'wrong' write wins. This makes timestamp generation and synchronization critically important—and surprisingly difficult in distributed systems.
The Magnitude of the Problem:
Consider two servers with a 100ms clock skew:
| Real Time | Server A Clock | Server B Clock |
|---|---|---|
| 12:00:00.000 | 12:00:00.000 | 12:00:00.100 |
| 12:00:00.050 | 12:00:00.050 | 12:00:00.150 |
If User A writes to Server A at real time 12:00:00.050 (ts = 12:00:00.050) and User B writes to Server B at real time 12:00:00.025 (ts = 12:00:00.125), then User B's earlier write has a higher timestamp and wins with LWW.
The semantically later write (User A's) loses. The earlier write (User B's) wins because of clock skew.
Clock skew doesn't cause errors or exceptions—it causes silently incorrect results. User A sees their write succeed, but minutes later it's replaced by User B's 'earlier' write with a higher timestamp. There's no indication anything went wrong. This is why timestamp accuracy is not just a performance concern but a correctness requirement.
Different systems use different approaches to generate timestamps, each with trade-offs between accuracy, complexity, and availability.
Physical (Wall-Clock) Timestamps
The most common approach: use the system's wall clock (typically synchronized via NTP).
Advantages:
Date.now())Disadvantages:
Mitigation Strategies:
12345678910111213141516171819202122232425262728
interface PhysicalTimestamp { wallClock: number; // System time (Date.now()) nodeId: string; // For tie-breaking} function createTimestamp(nodeId: string): PhysicalTimestamp { return { wallClock: Date.now(), nodeId, };} function compareTimestamps(a: PhysicalTimestamp, b: PhysicalTimestamp): number { // First, compare wall clock if (a.wallClock !== b.wallClock) { return a.wallClock - b.wallClock; } // Tie-breaker: lexicographic comparison of node IDs // This ensures deterministic ordering even with identical wall clocks return a.nodeId.localeCompare(b.nodeId);} // Example: Same millisecond, different nodesconst ts1: PhysicalTimestamp = { wallClock: 1699000000000, nodeId: "node-a" };const ts2: PhysicalTimestamp = { wallClock: 1699000000000, nodeId: "node-b" }; console.log(compareTimestamps(ts1, ts2)); // negative (node-a < node-b)// node-b wins the tie (higher in sort order)Many production databases default to or offer LWW as a conflict resolution strategy. Understanding how major systems implement LWW helps you make informed choices.
| Database | LWW Support | Timestamp Source | Notable Details |
|---|---|---|---|
| Cassandra | Default (column-level) | Client or coordinator timestamp | Each column has independent timestamp; can set per-write |
| DynamoDB | Available (with timestamps) | Application-defined | No built-in versioning; application must implement |
| CouchDB | Deterministic winner | Revision tree | Uses revision depth + hash for determinism |
| Riak | Optional (via config) | Vector clocks by default | Can enable LWW per bucket |
| Redis (CRDB) | LWW for strings | System timestamp | Enterprise geo-replication uses LWW |
| Azure Cosmos DB | LWW option | System or custom timestamp | Configurable conflict resolution policy |
Case Study: Cassandra's Cell-Level Timestamps
Cassandra applies LWW at the cell level (individual column values), not the row level. This means:
Example:
123456789101112131415161718
-- Initial rowINSERT INTO users (id, name, email) VALUES ('user1', 'Alice', 'alice@old.com') USING TIMESTAMP 1000; -- Later update to email only (timestamp 2000)UPDATE users USING TIMESTAMP 2000 SET email = 'alice@new.com' WHERE id = 'user1'; -- Concurrent update to name (timestamp 1500, arrives later due to network delay)UPDATE users USING TIMESTAMP 1500 SET name = 'Alicia' WHERE id = 'user1'; -- Final state after LWW resolution:-- id: user1-- name: Alicia (timestamp 1500 > 1000, wins over 'Alice')-- email: alice@new.com (timestamp 2000 > 1000, wins) -- Note: name and email were resolved independently!Cell-level LWW can create rows that no single write intended. If Write A sets {name: 'Alice', email: 'a@a.com'} at T1 and Write B sets {name: 'Bob', email: 'b@b.com'} at T2, and T2 > T1, you might expect Bob's row. But if only T2's email column wins due to replication timing, you could get {name: 'Alice', email: 'b@b.com'}—a Frankenstein row.
LWW's simplicity comes with significant trade-offs. Understanding these is essential for knowing when LWW is appropriate.
The Counter Problem Illustrated:
LWW is particularly dangerous for counter-like data:
| Step | Action | Value After | Timestamp |
|---|---|---|---|
| 1 | Initial | 0 | T0 |
| 2 | Client A: increment | 1 | T1 |
| 3 | Client B: increment (concurrent, didn't see T1) | 1 | T2 |
| 4 | LWW resolution: T2 > T1 | 1 | T2 |
Expected: 2 (two increments) Actual: 1 (one increment lost)
This is why CRDTs exist—for operations like increment, you need a strategy that can merge, not pick a winner.
Despite its trade-offs, LWW is the right choice in many scenarios. Use it when the following conditions apply:
| Data Type | LWW Suitable? | Reasoning |
|---|---|---|
| User profile fields | ✅ Yes | Typically single user updates their own profile |
| Last seen online | ✅ Yes | Latest timestamp is semantically correct |
| Cache invalidation | ✅ Yes | Latest invalidation should win |
| Device sync state | ✅ Yes | Current state matters, not history |
| Like/counter counts | ❌ No | Concurrent increments lose updates |
| Collaborative text | ❌ No | Merge is required, not selection |
| Financial transactions | ❌ No | Data loss is unacceptable |
| Shopping cart items | ⚠️ Maybe | Risk of items disappearing; union might be better |
Many systems use LWW for some fields and more sophisticated resolution for others. User settings might use LWW, while counters use CRDTs and financial data uses strong consistency. This 'polyglot conflict resolution' matches strategy to data semantics.
LWW is inappropriate for certain use cases. Recognizing these anti-patterns prevents data loss and frustrated users.
A social media platform used LWW for like counts stored as integers. When two users liked a post 'simultaneously,' both clients read count=50, incremented to 51, and wrote with their respective timestamps. LWW kept one write: final count = 51 instead of 52. For a viral post with thousands of concurrent likes, the displayed count could undercount by 20% or more. The fix: switched to a CRDT counter.
Alternative Strategies for LWW Anti-Patterns:
| Anti-Pattern | Better Strategy |
|---|---|
| Counter increments | G-Counter or PN-Counter CRDT |
| Set additions | G-Set or OR-Set CRDT |
| Collaborative text | Operational Transforms (OT) or YATA/Yjs |
| Audit data | Append-only log, event sourcing |
| Critical transactions | Strong consistency (2PC), saga pattern |
| Shopping cart merges | LWW-Element-Set CRDT or union merge |
Last-Write-Wins is the simplest and most widely deployed conflict resolution strategy. Its power lies in its simplicity; its danger lies in its silent data loss. Let's consolidate the key insights:
What's Next:
LWW can detect conflicts (by comparing timestamps) but resolves them by discarding data. What if we want to preserve enough information to actually merge conflicting writes? The next page explores Vector Clocks—a mechanism that tracks causality to enable smarter conflict resolution.
You now understand Last-Write-Wins comprehensively—its mechanism, timestamp challenges, production implementations, trade-offs, and appropriate use cases. You can now make informed decisions about when LWW fits your system's requirements.