All posts19 May 2026

Sync conflicts in plain English: last write wins, field merge, CRDTs

How offline apps settle conflicting edits: last write wins, field-level merge on per-field timestamps, or CRDTs. What each costs, and when each is enough.

W. Akram9 min readengineering

Offline-first apps settle conflicting edits in one of three ways. Last write wins keeps the newer copy of the whole record and drops the other without telling anyone. Field-level merge keeps both edits when they touched different fields, using a timestamp per field. CRDTs put the merge rule inside the data structure so replicas agree without a server deciding. Most business apps want the middle one.

A sync conflict is two copies of the same record changing independently before either has seen the other's change. What your system does at the moment those copies meet is a design decision. Skip it and you have chosen last write wins, because the last request to reach the database is the one that sticks.

Last write wins, and the edit nobody sees disappear

Every record carries a version number or an updated timestamp, and when two copies meet the newer one replaces the older one whole. It costs nothing to build: it is what you already have if your API accepts entire objects on a PUT.

The failure is quiet and total. The office adds a note to a job at ten past nine. An engineer's phone, offline since eight, syncs at twenty to twelve with the readings filled in. The engineer's copy is newer, so it lands whole and the note is gone. No error, nothing in the log saying a field was overwritten with an older value. Whoever wrote the note finds out next week, if ever.

The word doing the damage is "last". Mean the client's clock and you are trusting a device that has been in a van since Tuesday and may have had its time set by hand. Mean the order writes reached the server and "last" becomes "whoever got a signal first", which for two engineers walking out of two different basements is close to a coin toss.

None of which stops it being the right answer more often than engineers like to admit. Where a record has one owner at a time, there is nothing to merge, and per-field metadata buys you storage costs and nothing else.

Field-level merge, and why it is the sensible default

Keep a timestamp per field rather than per record. On merge, take each field from whichever copy carries the later stamp for it. Two people editing different parts of the same job both keep their work. Only a real collision, the same field written on both sides, needs a decision, and there you fall back to newest-wins on that one field or keep both values and put the record in front of a human.

The cost lands on the write path rather than the merge. Whole-object updates have to go, because a client that sends you the entire record cannot tell you which fields it meant to change. You need partial updates, or an event per change naming the fields it touched. And you have to decide what counts as a field: a nested list is not one, and per-field stamps do nothing sensible for an array where one device inserted an item and another reordered them.

One thing that saves arguments in planning meetings. Per-field last write wins is a CRDT. A map of last-write-wins registers is one of the standard convergent types in Shapiro, Preguiça, Baquero and Zawirski's 2011 catalogue for INRIA, A comprehensive study of Convergent and Commutative Replicated Data Types. You get the convergence property without the library or the wire format. So when somebody says the project needs CRDTs, ask which one. Often the honest answer is this one, and you can build it in an afternoon.

CRDTs, and the machinery that comes with them

We have never shipped one in a business app and have twice talked a client out of starting with one, which is the bias to read this section through.

A conflict-free replicated data type carries its own merge rule, so any two replicas that have seen the same set of updates land on the same value in whatever order those updates arrived, with no server arbitrating. The 2011 INRIA catalogue is worth an evening if only to see how much design goes into making "delete" behave.

What you pay for the guarantee:

  • Metadata that grows with edit history. Removals leave tombstones behind, and every compaction strategy is its own project.
  • A data model that stops matching your API. Rows stop being rows, and reading the store to work out what happened needs a viewer you write yourself.
  • A commitment to a library and a format. Changing your mind two years in means migrating a log, not writing an ALTER TABLE.
  • Convergence is not correctness. Two devices agreeing on a merged value does not make it a legitimate record. A compliance form with half its answers from one engineer and half from another, stitched together automatically, is indefensible in front of an auditor even though both replicas hold it identically.

Where they earn the money: free text and ordered lists that several people edit at the same moment. Shared documents, whiteboards, anything with other people's cursors in it. There the merge behaviour is the product, and rolling your own is worse than adopting a mature library.

The three side by side

Last write winsField-level mergeCRDT
What survivesThe newer copy of the whole recordEvery field only one side touchedEvery update, merged by rule
Build costA timestamp columnPer-field metadata, partial updatesA library, a format, a new data model
Running costNoneWider payloadsTombstones, compaction, custom tooling
Where it breaksTwo roles edit different fields of one rowThe same field on both sides, and ordered listsMerged values that are valid data and invalid records
Enough whenOne owner at a timeStructured records, few simultaneous editorsSimultaneous free-text or list editing

Read the bottom row first. Nearly every business system we have built sits in the middle column, edited by a handful of people who are rarely inside the same field at the same second. The right-hand column is a different product shape, and paying for it early is how a team ends up with a data layer nobody can debug on a Friday.

Whatever you land on, write the behaviour down in one sentence a support person can say out loud to a customer. We have had that sentence take a morning to agree, which is usually the sign the design was never settled in the first place.

What we built on a field operations platform, and why it was not a CRDT

On a field operations platform we built for a regulated compliance industry, the conflict we spent our design time on was not the one the textbooks describe.

Engineers work in basements and plant rooms with no signal. Every change they make is an append-only, client-timestamped event in a local outbox in the browser's IndexedDB, and the job they see on screen is a projection of replaying that job's own events. Nothing waits for a server. The sync engine drains the outbox per job in FIFO order, plus explicit dependency edges between queued writes: a form submission depends on the event that started the visit, because the endpoint resolves by a server id that does not exist until the start call returns, and it depends on the upload of every photo it references.

That is why we did not reach for CRDTs. The problem was ordering, not merging: a queued write could not be applied until an earlier one came back carrying an identity. A CRDT would have converged the data beautifully and told us nothing about which write had to go first, so we would have paid for the machinery and built the dependency graph by hand anyway.

We are also less sure than we sound that this generalises. It leans on one job being worked by one engineer at a time, which holds in this domain and would not hold in a warehouse or on a ward.

When the right answer is to refuse the merge

The engineer owns the evidence, meaning the readings, the photos and the signature at the end. The office owns the schedule: assignment, dates, cancellation. When those two collide on the same job we do not merge, we refuse.

A job cancelled by the office while it holds unsynced local work gets flagged and kept. It stays visible in the engineer's list and goes read-only with a notice saying to check with the office. A job with no local work on it is stale cache and disappears quietly. Auto-discard was rejected because the office cancelling a job has no idea a reading was captured twenty minutes ago in a basement. So was a modal demanding an immediate keep-or-discard answer, which is a destructive decision to put on a wet tablet screen.

The rule underneath all of it: local work is never deleted by a server-side change, only by a successful sync or by the user deliberately wiping it. There is one local database per user, keyed on the identity in their token, and logging out never deletes that either. Tablets get handed between people, and someone whose session lapses in a dead zone must not lose a day's evidence.

Common questions

Should the app ask the user to resolve a conflict?

Rarely, and almost never on a phone in a plant room. A prompt forces a decision at the worst moment, with the least context, from the person with the most to lose. Preserve both versions, mark the record and route it to somebody sitting at a desk.

Is field-level merge just a simple CRDT?

Effectively, yes. A map of last-write-wins registers is one of the convergent types in the original 2011 catalogue. Build it yourself and you keep your own schema, your own API and your own debugging tools, paying in per-field metadata rather than in a library and format commitment.

Whose clock decides which write came later?

Not the device's, if you can help it. Stamp writes on the client so you know the order the work happened in, then have the server record its own receipt time and use that for anything a person will be judged by.

How do you test conflict handling?

Two devices in aeroplane mode, a scripted pair of edits, then reconnect them one at a time and swap which one goes first. Run it again with both edits in the same field. Conflict bugs are order-dependent, so a suite that only ever replays one order proves very little.

Let's talk

Ready to build the thing?

Book a free 30-minute call. We'll dig into your idea, your stack and your timeline, and give you an honest read on what it will take to build and launch. You'll leave with a clearer plan whether or not you hire us.

Free 30-min call. No pitch.