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:
Direct Mutation Endpoints​
When using direct mutation endpoints (e.g., POST /v1/bookmarks), the same conflict rules apply:
- Include
lastMutationAtas a query parameter - Check for 409 responses
- Update
lastMutationAtfrom theX-Mutation-Atresponse header
Store the X-Mutation-At header value after every successful mutation. If you miss it, use metadataOnly=true to recover.
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