advanced 3 min answer

A collaborative field-notes app uses a CRDT set per project so devices can edit offline and merge without a server decision. Two years in, projects that are heavily edited take 40 seconds to open on a mid-range phone and the local database is 900 MB. Nothing has failed. What is happening, and what would you have designed differently?

crdtstombstonesmetadatafigmaoffline-first
Show the full answer Hide the answer

Second by second, what happens on open

The device loads the CRDT state, which is not the current value of the set. It is the current value plus the evidence needed to merge correctly with a replica it has not seen yet. For a set that supports removal, that evidence includes a record of every element ever removed — tombstones — because a replica that never saw the removal must not resurrect the element. Add per-element causal metadata, and a project with 50,000 edits carries far more metadata than data.

Opening the project means deserialising all of it, which is CPU and allocation bound on the phone. 40 seconds is the parse, not the network.

Where it amplifies

Three multipliers, and they compound:

  • Metadata per element does not shrink when the element is deleted. A project that churns items is worse than one that only adds.
  • Every replica keeps the whole history, so the cost is paid on every device, including the ones that joined last week and only need today's data.
  • Sync payloads grow with history, not change, unless the protocol supports deltas keyed on what the peer already has. Teams usually discover this after the first year, when a new device's initial sync starts timing out.

What stops it

Garbage collection of tombstones requires knowing that every replica has seen the removal, which is exactly the coordination a CRDT was chosen to avoid. The practical mechanisms all reintroduce a bounded amount of it:

  1. A membership horizon. Replicas that have not synced within N days are declared stale and must re-bootstrap from a snapshot. Now removals older than N days can be collected. This is the single highest-value design decision, and it must be made on day one, because retrofitting it invalidates devices in the field.
  2. Snapshot plus recent operations. Ship a compacted state as of a watermark and only the operations after it. New devices download the snapshot, not the history.
  3. Scope the CRDT smaller. One CRDT per project, not per workspace, so the cost is bounded by the unit a user actually opens.
  4. Choose the weaker structure deliberately. Many fields do not need concurrent-edit semantics. A last-writer-wins register with a server timestamp is a tenth of the metadata, and for a "status" field it is the behaviour users expect anyway.

When a CRDT is the wrong answer

When Figma described its multiplayer design in 2019 it explained that it did not use a full CRDT: because there is always a server in the picture, the server can be the authority on ordering, which removes most of the metadata and all of the convergence proofs. The precondition for a CRDT is not "we want offline editing" — it is "there are times when no single node can be the decision-maker". A mobile app that is offline for hours but always syncs through your backend has an authority available; it just is not present at the moment of the edit.

What would have to be true to self-heal

Nothing in the data model self-heals: history only grows. The system recovers only if some component is allowed to declare a boundary and forget. A design with no forgetting mechanism has a latent, monotonic failure whose arrival date is set by user enthusiasm.