
Offline-First Sync Conflicts: Debug Mobile Data Loss
Data sync conflicts are the silent killers of offline-first mobile apps. When your app works perfectly online but corrupts user data the moment connectivity drops, you have an offline-first sync debugging problem — and it's one of the hardest classes of bugs to catch before production. Unlike crashes that leave stack traces, sync failures often manifest as missing records, duplicate entries, or silently rolled-back changes that users discover days later. This guide walks through detecting, debugging, and resolving the most common mobile data sync failures across Firestore, Realm, SQLite, and custom implementations.
Why Offline-First Sync Breaks
Offline-first architecture promises seamless user experiences regardless of connectivity, but it introduces a fundamental tension: your app must serve data instantly from a local store while eventually converging with a remote source of truth. When those two states diverge, you get conflicts.
The most common sync failure modes include:
- Write-write conflicts: Two devices modify the same record while offline, and the sync protocol must choose which version wins
- Partial sync failures: Only some records in a batch write successfully, leaving the local database in an inconsistent state
- Ordering violations: Operations replay in the wrong sequence, breaking causal dependencies (e.g., deleting a parent before its children are synced)
- Clock skew: Device clocks drift, causing timestamp-based conflict resolvers to pick the wrong winner
- Schema mismatch: A field added in a new app version doesn't exist in the remote schema yet, causing deserialization failures
According to Google's Firestore documentation, offline persistence uses a local queue that replays writes when connectivity resumes, but the queue can overflow or encounter security rule rejections that were valid at write time but invalid by replay time Firestore Offline Data. These "silent rejection" scenarios are particularly dangerous because the write appears successful locally but never reaches the server.
Common Sync Architectures and Their Failure Modes
Firestore Offline Persistence
Firestore's offline-first model is the gold standard for many mobile teams. When enabled, writes go to a local cache immediately and are queued for server synchronization. The SDK handles retry logic, but it can fail silently in several ways:
- Queue depth limits: Firestore caps the pending write queue. On Android, the default is 500 writes; exceeding this causes
FAILED_PRECONDITIONerrors that many developers don't handle - Indexing gaps: A query that works with local cache may require a composite index on the server — the local result differs from the remote result
- Security rule time-drift: A write allowed at
request.timeduring offline may be rejected on replay if the rule evaluates against server time
To debug Firestore sync, enable verbose logging with FirebaseFirestore.setLoggingEnabled(true) on Android or Firestore.enableLogging(true) on iOS. The logs reveal pending write counts, sync status transitions, and backend error codes that are otherwise swallowed Firestore Debug Logging.
Realm / MongoDB Device Sync
Realm's Atlas Device Sync uses a fundamentally different approach: it syncs objects at the field level using an operational transform. The default conflict resolver is last-write-wins, but you can implement custom resolution logic MongoDB Realm Sync.
Common Realm sync failures include:
- Subscription set mismatches: Client subscriptions filter which objects sync. If the subscription query changes between app versions, previously synced objects may vanish locally
- Compensating writes: When the server rejects a write due to permissions, Realm undoes the local change with a "compensating write" — but this can cascade if dependent objects were already modified
- Session termination on schema changes: Breaking schema changes in Atlas Device Sync terminate all client sessions until clients update
Debug these with Realm Studio's sync log viewer, which shows per-object sync history, conflict events, and session state transitions. The SyncSession.addProgressNotification() API also exposes sync progress programmatically.
Custom SQLite + REST Sync
This is the most common pattern for teams not using a managed sync service — and the most fragile. A typical implementation stores data in SQLite and syncs via REST API calls when connectivity is available. Without a battle-tested sync engine, developers must handle every edge case manually:
- Tombstone propagation: When a record is deleted, a tombstone marker must sync to all devices. Missing tombstones cause "zombie records" that reappear after sync
- Partial transaction commits: A batch of related writes (e.g., order + line items) must sync atomically. A network failure mid-batch leaves the local database inconsistent
- Surrogate key collisions: Auto-incrementing IDs conflict when multiple offline clients create records independently. UUIDs prevent this but complicate server-side indexing
The SQLite documentation on atomic commits explains the ACID guarantees that make local storage reliable, but those guarantees don't extend across the network boundary — you must implement your own idempotency and conflict detection layer.
Detecting Sync Failures Before Your Users Do
Sync failures are invisible to traditional crash reporting tools because they rarely throw exceptions. Instead, they manifest as data corruption that users report days later. To catch them proactively, instrument every sync operation:
Structured Sync Logging
Every sync attempt should log: operation type (create/update/delete), entity type, local timestamp, server timestamp on success, error code on failure, and the conflict resolution outcome. Structured logging lets you query for patterns — for example, a spike in conflict rates for a specific entity type often signals a schema change that wasn't backwards-compatible.
For structured mobile logging patterns, see our guide on mobile app logging best practices.
Key Sync Health Metrics
Track these metrics in production using custom events:
- Sync latency (p50/p95/p99): Time from local write to confirmed server sync
- Conflict rate: Conflicts per sync operation, broken down by entity type
- Queue depth: Number of pending writes awaiting sync (rising queue depth = connectivity or throughput issue)
- Sync success rate: Percentage of sync attempts that succeed on first try
- Silent failure rate: Writes marked successful locally but never confirmed server-side (requires periodic reconciliation queries)
Bugspulse custom event tracking lets you instrument these metrics with minimal code. When sync latency spikes or conflict rates exceed your baseline, Bugspulse alerts your team before users notice data corruption. Session replay with privacy-first masking lets you watch exactly what the user saw during the sync failure — no need to reproduce the network conditions yourself.
Debugging Sync Conflicts: A Step-by-Step Process
1. Reproduce the Conflict Reliably
The hardest part of sync debugging is reproduction. Network conditions are inherently unpredictable. Use these tools to simulate realistic failure scenarios:
- iOS Network Link Conditioner: Built into Xcode's Additional Tools, this lets you simulate 3G/Edge latency, packet loss, and DNS delays. Enable it, perform offline writes, toggle connectivity, and watch what happens
- Android Emulator Cellular Settings: Configure network type, signal strength, and latency per AVD
- Charles Proxy / Proxyman: Inspect the actual HTTP payloads flowing during sync. Look for partial responses, 4xx errors on replay, and mismatched timestamps. Both tools support SSL proxying for encrypted traffic Charles Proxy
2. Inspect the Conflict Data
Once you reproduce the conflict, compare local and server state side by side:
- Firestore: Use the Firebase Console's Data viewer to see the server-side document. Compare with
documentSnapshot.getData()from your debug build - Realm: Realm Studio shows both the local realm file and the synced state, with a diff view for conflicting objects
- SQLite: DB Browser for SQLite or Android Studio's Database Inspector let you query the local database directly. Compare with server state from your admin dashboard or direct API calls
Log both sides of every conflict — the local value, the server value, and the resolution outcome. This audit trail is invaluable for identifying patterns: are certain entity types responsible for 80% of conflicts? Are conflicts clustered around app version upgrades?
3. Choose and Implement a Resolution Strategy
There are three approaches, in order of increasing complexity:
Last-write-wins (LWW): The simplest strategy — whichever write has the later timestamp wins. Works for data where conflicts are rare and overwrites are acceptable (e.g., user preferences). Firestore and Realm default to LWW. The risk is silent data loss if two users edit the same record.
Field-level merge: Instead of treating the entire record as a conflict unit, merge at the field level. If User A changed the title field and User B changed the status field, both changes survive. This requires tracking which fields were modified locally — a dirtyFields set stored alongside the record.
CRDT-inspired approaches: Conflict-Free Replicated Data Types guarantee eventual convergence without a central authority. Libraries like Automerge and Yjs implement CRDTs that work across devices. For mobile, Automerge provides a lightweight Rust core with Swift/Kotlin bindings. CRDTs are overkill for simple key-value data but essential for collaborative editing or complex offline workflows CRDT Guide.
Manual Conflict Resolution with User Prompts
For user-facing data (notes, tasks, documents), prompt the user when a conflict is unresolvable automatically. Show both versions with a simple diff view and let the user choose. Store the resolution decision so you can apply the same logic automatically next time. Always log the resolution choice — it's part of the audit trail.
Preventing Sync Failures in Production
Schema Versioning and Migration Compatibility
Every schema change must account for clients running old versions. Strategies include:
- Additive-only changes: New fields default to null. Old clients ignore unknown fields (most JSON parsers do this by default)
- Deprecation windows: When removing a field, keep the server accepting it for N app versions, then drop it
- Schema version field: Include a
schemaVersionin every record. Clients can handle multiple versions during the migration window
Firebase Firestore is schemaless, which simplifies additive changes but means validation happens in security rules — old rules may reject new field shapes. Realm's schema is strict, so use @Persisted with default values for new fields and never remove a field without a migration block MongoDB Schema Migration.
Graceful Degradation During Sync
When sync is pending or failing, don't leave users staring at spinners. Show stale data with a "syncing..." indicator. Queue user actions optimistically — let them create, edit, and delete while offline, with clear visual indicators of pending changes. If sync fails permanently, surface the conflict to the user rather than silently losing their work.
Chaos Engineering for Sync
Test your sync resilience with deliberate chaos:
- Network throttling: Reduce bandwidth to 50kbps and add 500ms latency. Does sync still complete eventually?
- Force-kill during sync: Terminate the app mid-sync and relaunch. Does the local database recover to a consistent state?
- Rapid online/offline toggling: Switch airplane mode on and off rapidly. Does the sync queue handle the flapping?
- Concurrent write testing: Run multiple simulator instances modifying the same data simultaneously. Does conflict resolution handle every case?
For a comprehensive approach to pre-release testing that catches issues before users do, read our mobile app crash prevention guide.
Tools and Libraries for Sync Debugging
| Tool | Best For | Platform |
|---|---|---|
| Firebase Emulator Suite | Local Firestore sync testing | iOS, Android, Web |
| Realm Studio | Visual sync inspection and conflict review | macOS, Windows, Linux |
| DB Browser for SQLite | Local SQLite database inspection | macOS, Windows, Linux |
| Charles Proxy | HTTP sync payload inspection | macOS, Windows |
| Proxyman | Modern HTTP debugging with native macOS UI | macOS |
| Bugspulse Custom Events | Production sync health monitoring | iOS, Android, React Native, Flutter |
| Network Link Conditioner | Network simulation for sync testing | iOS, macOS |
The Firebase Emulator Suite deserves special attention — it runs a local Firestore instance that supports offline persistence and security rules evaluation. You can test your entire sync pipeline without deploying to production, including conflict scenarios with multiple emulator instances Firebase Emulator Suite.
Building Confidence in Your Sync Layer
Offline-first data sync is one of the hardest problems in mobile development, but it doesn't have to be a source of user-facing data loss. By instrumenting every sync operation with structured logging, tracking key health metrics in production, and testing with realistic network conditions, you can catch sync failures before they corrupt user data.
Bugspulse helps mobile teams monitor sync health alongside traditional crash and error tracking. Custom events track sync latency, conflict rates, and queue depth — all surfaced in real-time dashboards. When a sync failure does occur, privacy-first session replay shows exactly what the user experienced, so you can reproduce and fix the issue without guessing.
Start monitoring your app's sync health today — sign up at app.bugspulse.com/register and ship with confidence.