Lab Findings / Sync Architecture
CloudKit CRDT Limits: Where NSPersistentCloudKitContainer Breaks
NSPersistentCloudKitContainer handles the majority of sync scenarios correctly. Here are the three specific scenarios where it does not — and the practical threshold for deciding when to build past it.
Lab Finding · 3NSOFTS · January 2026 · Status: Research completeWhat CloudKit handles correctly
NSPersistentCloudKitContainer implements a last-writer-wins (LWW) merge strategy for Core Data models. For most applications, this is correct behavior:
- —Sequential edits from a single user across devices sync reliably and converge to the correct state
- —Offline edits queue correctly and apply when connectivity resumes without data loss
- —Insert conflicts (two devices creating records with the same logical key) are detected and resolved
- —Relationship integrity is maintained across sync boundaries
For single-user apps syncing across personal devices (iPhone, iPad, Mac), CloudKit's built-in sync is sufficient for the overwhelming majority of data models. The scenarios below are real limitations, but they apply to a specific category of use case.
Failure scenario 1: Collaborative numeric counter fields
Consider an inventory count field where two users both decrement stock simultaneously from different devices while offline. User A: 10 → 8 (ships 2 units). User B: 10 → 6 (ships 4 units). The correct merged value is 4 (10 − 2 − 4). CloudKit's LWW merge produces either 8 or 6 depending on which write arrives last — discarding one user's operation entirely.
This is the classic lost-update problem. A CRDT-appropriate solution is a PN-Counter (positive-negative counter) — each device tracks its own increment/decrement delta, and the merge operation sums all deltas rather than replacing with the latest value. CloudKit does not support this natively.
Scope: Affects apps with shared numeric state that multiple users modify concurrently while offline. Single-user apps with numeric fields are not affected — serial writes converge correctly.
Failure scenario 2: Multi-user shared set operations
Two users simultaneously add different items to a shared set (a to-do list, a tag set, a permission roster) while offline. User A adds item X. User B adds item Y. Both sets should merge to contain both X and Y. CloudKit's LWW merge replaces the entire set with whichever arrived last — one addition is lost.
The CRDT solution is an OR-Set (add-wins set) — additions are tagged with a unique timestamp and merged by union; removals only remove entries with a specific tag, not all entries with that value. This is well-understood theory. Implementing it on top of Core Data requires custom merge policies and a schema designed around tracking operations rather than state.
Scope: Affects collaborative list/set data structures with concurrent additions across users. The Company App encountered this at the boundary of its data model — the mitigation was to partition write access by user rather than solve the merge problem.
Failure scenario 3: Causal ordering requirements
CloudKit sync does not preserve causal ordering across records. If a record update causally depends on another record (e.g., a reply to a message, a correction to an order), there is no guarantee that the dependency arrives before the dependent record. In practice, the network usually delivers records in causal order because of how CloudKit batches changes — but this is not guaranteed, and the failure mode (a reply visible before the message it replies to) requires UI and logic handling that CloudKit does not provide.
This affects primarily: messaging, comment threads, event sourcing patterns, and any data model where the correctness of a record depends on the presence of another record.
What we researched: hybrid architecture
For the scenarios above, the options are:
- Work around the limitation. Partition write access so concurrent writes become sequential (each user owns specific records). Effective for many collaborative patterns, does not require CRDT implementation. This is what the Company App does.
- Use a custom merge policy.
NSMergePolicycan be customized in Core Data. You can implement delta-based merging for specific entity types while retaining CloudKit sync for others. Requires careful testing; the policy runs after CloudKit delivers changes, not during delivery. - Move to a custom backend. At the point where you need full CRDT semantics, you have outgrown CloudKit. A custom backend with a CRDT library (Automerge is the most mature Swift-compatible option) handles these patterns correctly. The cost is infrastructure, maintenance, and the elimination of the zero-backend advantage that makes CloudKit compelling for early-stage apps.
Approaches we abandoned
- ✗Custom CloudKit CKRecord operations as a CRDT layer. In theory you can build a CRDT on top of raw CloudKit using CKRecord field-level operations and custom merge logic in a CloudKit subscription handler. In practice, the complexity of coordinating CloudKit push notifications, managing record zones, and implementing merge logic in the subscription handler made this more complex than a custom backend while being less reliable. Abandoned after prototype.
- ✗Operational transforms via CloudKit functions. CloudKit does not support server-side logic (no equivalent to Firestore rules or Supabase Edge Functions). Transforming operations server-side is not possible without a backend process, which moves you back to a custom infrastructure model.
Practical threshold recommendation
Use NSPersistentCloudKitContainer if:
- ✓Your app is primarily single-user syncing across personal devices
- ✓Collaborative writes can be partitioned (each user owns their records)
- ✓Lost updates on concurrent numeric edits are infrequent or acceptable in your domain
Move past CloudKit if:
- →Multiple users concurrently modify shared numeric state (inventory, balances, counters)
- →You need collaborative editing of shared document or structured content
- →Causal consistency is a correctness requirement (threaded discussions, event sourcing)
