Conflict Resolution Strategies
Last-Write-Wins (LWW)
Read aloud is unavailable in this browser.
The Simplest Resolution: Let Time Decide
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.
The LWW Mechanism
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:
- On Write: Attach a timestamp to the data being written
- On Receive: Compare the incoming timestamp to the existing timestamp
- If Incoming > Existing: Accept the new value, discard the old
- If Incoming ≤ Existing: Reject the new value, keep the old
- On Conflict Detection: The higher timestamp always wins
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.
The Timestamp Challenge
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.
- •Clock Drift — Physical clocks drift over time. Without synchronization, two servers' clocks can differ by seconds or even minutes. A slow clock means 'old' writes appear 'new' to that server.
- •NTP Jitter — NTP (Network Time Protocol) synchronizes clocks but has accuracy limits (~50ms over internet, ~1ms on LAN). Writes within the jitter window have ambiguous ordering.
- •Clock Skew Between Nodes — Even with NTP, different nodes have slightly different clock values at any instant. Writes 1ms apart in wall-clock time might have inverted timestamps.
- •Clock Discontinuities — NTP can adjust clocks backward (if a clock was fast). This can cause a newer write to have an 'older' timestamp than a previous write on the same node.
- •Timezone Mistakes — Using local time instead of UTC can cause massive timestamp errors across geographic regions.
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.
Timestamp Sources and Strategies
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:
- Simple to implement (just call
Date.now()) - Human-readable (debug-friendly)
- No additional infrastructure needed
Disadvantages:
- Subject to all clock synchronization issues
- Can move backward (after NTP adjustment)
- Accuracy limited by NTP (~1-50ms)
Mitigation Strategies:
- Use monotonic clocks where available (cannot move backward)
- Combine with logical counters (like HLC, below)
- Accept that 'ties' are possible and break them consistently (e.g., by node ID)
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)LWW in Production Systems
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:
- Different columns in the same row can have different 'winning' writes
- Each mutation carries a timestamp for affected cells
- Reads compare timestamps per cell
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.
The Trade-offs of LWW
LWW's simplicity comes with significant trade-offs. Understanding these is essential for knowing when LWW is appropriate.
- •Simplicity — Easy to understand, implement, and debug. No complex merge logic.
- •Deterministic — Given same inputs, all replicas reach the same state. No randomness.
- •Low Overhead — Only need to store one timestamp per value. Minimal metadata.
- •Always Converges — Replicas will eventually agree, guaranteed.
- •No User Intervention — Resolution is automatic. No conflict dialogs.
- •Composable — Works with any data type. No special data structure requirements.
- •Data Loss — Losing writes are completely discarded. No merge, no history.
- •Clock Dependency — Correctness requires accurate, synchronized clocks.
- •Semantic Blindness — Ignores what operations mean. Increment vs. overwrite treated same.
- •Silent Failures — Users don't know their write was overwritten.
- •Counter Problem — Concurrent increments lose updates (see below).
- •Non-Causal Wins — With clock skew, causally earlier writes can win.
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.
When to Use LWW
Despite its trade-offs, LWW is the right choice in many scenarios. Use it when the following conditions apply:
- •Writes are naturally non-conflicting — Each piece of data is typically written by only one source (e.g., user-specific settings, device-specific data). Conflicts are rare exceptions.
- •Last value is what matters — For status updates, sensor readings, or current state queries, the latest value is semantically correct. History isn't important.
- •Data loss is tolerable — For caching, session data, or non-critical state, losing an update is annoying but not catastrophic.
- •Conflicts are infrequent — With good data partitioning, real conflicts are rare. LWW handles the edge cases acceptably.
- •Clock synchronization is reliable — You have good NTP configuration, or you're using a single logical time source, or writes are seconds apart (not milliseconds).
- •Simplicity is a priority — You don't have the engineering capacity for complex resolution logic. LWW is 'good enough.'
| 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.
When to Avoid LWW
LWW is inappropriate for certain use cases. Recognizing these anti-patterns prevents data loss and frustrated users.
- •Operations should merge, not replace — For counters, sets, lists, or any data where concurrent updates should combine their effects, LWW causes data loss.
- •Every update is valuable — For audit logs, event streams, or any data where no write should ever be lost, LWW is dangerous.
- •User intent matters — For collaborative editing where users expect their changes to coexist (not overwrite), LWW destroys work.
- •Clock synchronization is poor — If you can't guarantee reasonable clock sync, LWW resolution may be semantically wrong.
- •Conflicts are frequent — If your data model causes many real conflicts, LWW's data loss becomes a significant problem.
- •Regulatory requirements apply — For financial, medical, or legal data, losing writes may violate compliance requirements.
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 |
KEEP LEARNING
There’s more to understand.
You’re reading the introduction. Create a free account to read the complete lesson, save notes and keep track of your progress.