Realm's Offline-First Sync: What Actually Happens When the Network Comes Back
Realm sync reconciles offline edits automatically using CRDT-style merging, but schema strictness and heavy initial sync are the real costs. Here's how it works and how to verify it.
20 Apr 2026, 15:08 UTC

Your users will edit data in elevators, on airplanes, and in parking garages. The question isn't whether your app works offline — it's what happens when two devices edited the same record while disconnected and both reconnect. Realm's answer is automatic conflict resolution built on CRDT-style merge semantics, and it's the main reason teams pick it over hand-rolled sync layers.
The thesis: Realm sync removes the hardest part of offline-first mobile development — reconciling divergent edits — but it trades that convenience for constraints on schema changes and a potentially heavy first sync. Understanding both sides keeps you out of trouble.
How offline edits reconcile without a conflict screen
Realm sync is built around conflict-free replicated data type (CRDT) ideas: every local change is recorded as an operation, and operations from different devices are designed to merge deterministically. When a device reconnects, it uploads its pending operations and downloads everyone else's. The server applies a deterministic ordering, so every client converges on the same state without you writing merge logic.
The practical consequence: two users editing different fields of the same object both win. Two users editing the same field resolve by a last-write-wins rule at the operation level, deterministically, so all devices agree on the outcome. You don't get a conflict dialog, and you don't get divergent replicas — but you also don't get to choose the merge policy. If your domain needs "reject the second booking for the same seat," that's application logic you enforce with a server-side function or validation, not something sync does for you.
Queries stay local, and that's the point
Everything you query in Realm runs against the local embedded database. There is no "remote query" path in normal use — sync keeps the local replica current in the background, and reads are lazy, loading matching objects on demand rather than materializing entire result sets into memory. On memory-constrained devices this matters: iterating a large result set doesn't pull every object into the heap at once.
This architecture has a design implication people miss: you can't query data the user hasn't synced. With partition-based or flexible sync, you define subscriptions that determine which data lives on the device. A query for something outside the subscription returns nothing — not an error, just empty results. Debugging "my query returns zero rows" often means checking the subscription, not the query.
A worked example: offline edit and reconnect
Here's the shape of the flow using the Swift SDK (the Kotlin and JavaScript SDKs follow the same pattern). Assume a synced Realm with a Task model and flexible sync already configured:
// Runs on the device; user must be logged in with SyncUser.current
let realm = try await Realm(configuration: user.flexibleSyncConfiguration())
// Works identically online or offline — writes go to the local store
try realm.write {
task.status = "done"
task.updatedAt = Date()
}
// If offline, the change queues locally. On reconnect, sync uploads it automatically.No connectivity check, no retry queue, no merge code. To verify the behavior end to end: put the device in airplane mode, make the write, kill the app, relaunch (confirm the change persisted locally), disable airplane mode, and check the server-side data browser in your Atlas App Services dashboard. You should see exactly one version of the object with the new value — no duplicates, no conflict artifacts.
Schema changes: the part that bites
Realm's schema evolution is forgiving in one direction and strict in the other. Adding a new optional property requires no migration — old clients simply ignore it. But renaming a property, deleting one, or adding a required property without a default will throw on app launch unless you supply a migration block. In a synced app this is sharper still, because schema changes must stay compatible across every client version in the field. A user on last year's build still has to open the Realm.
The safe playbook: only add optional properties, never rename (add a new property and copy values in a migration if you must), and test migrations explicitly. A practical check: add a required property to a model in a debug build, relaunch against an existing local Realm file, and confirm your migration block runs without data loss before shipping. If you see an exception at startup mentioning a schema version mismatch, that's the missing-migration case.
The trade-off: first sync is expensive
The cost of automatic reconciliation is that a new device needs the data before it's useful. Initial sync downloads the full dataset within the user's subscriptions before applying incremental changes. Over slow connections, a large dataset means a long first-launch experience. Mitigations are real but require planning: keep subscriptions narrow, archive old data server-side, and design the onboarding flow to show progress rather than a dead spinner.
The actionable takeaway: adopt Realm sync when your conflict-resolution needs are simple (field-level last-write-wins is acceptable) and your per-user dataset is modest. Verify it with the airplane-mode test above and a migration test on every schema change. If you need custom merge logic or must sync huge datasets to new devices instantly, plan those as explicit engineering work — sync won't do them for you.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.