Handling Sync Conflicts
When your client's lastMutationAt doesn't exactly match the server's state, you'll receive a 409 Conflict error. This guide explains when conflicts occur and how to resolve them.
When Conflicts Occur
A 409 OutOfSyncError happens when:
- Another device synced — User made changes on a different device
- Concurrent mutations — Server received mutations while you were offline
- Missed header — Client didn't capture the
X-Mutation-Atresponse header - First sync mismatch — Sending wrong
lastMutationAtfor a new user
The 409 Error Response
When a conflict occurs, the server responds:
- Error code:
OutOfSyncError - Message:
Invalid lastMutationAt, please re-sync your data and try again.
HTTP Status: 409 Conflict
First-Sync Variant
If this is a new user's first sync and you send the wrong lastMutationAt:
- Error code:
OutOfSyncError - Message:
First sync detected. Please use lastMutationAt=-1 for initial sync.
Conflict Resolution Flow
Here's the standard recovery pattern:
Step-by-Step Recovery
- Catch the 409 error
- Fetch server changes —
GET /v1/sync?mutationsSince=<your_lastMutationAt> - Apply server mutations to your local database
- Resolve conflicts if the same resource was modified both locally and remotely
- Retry your mutations with the new
lastMutationAt
Code Example
For production-ready implementations, see the SDK docs.
Conflict Resolution Strategies
When the same resource is modified both locally and on the server, you need a strategy:
1. Server Wins (Recommended for simplicity)
Server changes always take precedence. Discard conflicting local changes.
2. Client Wins
Local changes always take precedence. Re-submit all local mutations.
3. Last-Write Wins
Compare timestamps and keep the most recent change.
4. Merge (Complex)
Combine changes field-by-field. Best for resources with independent fields.
Recovering lastMutationAt
If your client loses track of lastMutationAt (e.g., app crash, missed header), use metadataOnly:
This quickly retrieves the current lastMutationAt without fetching all mutations. You still need to pass mutationsSince in the request.
See the full request/response schema in the API reference:
Best Practices
- Always handle 409 — Don't assume sync will succeed
- Queue mutations locally — Store pending changes before attempting sync
- Implement retry logic — Automatic recovery improves UX
- Choose a consistent strategy — Pick one conflict resolution approach and stick with it
- Log conflicts — Track how often conflicts occur to optimize your sync frequency
Next Steps
- Offline-First Patterns — Architecture for robust offline support