| id | handling-conflicts |
|---|---|
| title | Handling Sync Conflicts |
| sidebar_label | Handling Conflicts |
| sidebar_position | 2 |
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.
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
sequenceDiagram
participant Phone as Phone A
participant Server as Server
participant Tablet as Tablet B
Phone->>Server: POST /v1/sync?lastMutationAt=100
Server-->>Phone: 200 OK, lastMutationAt=200
Note over Tablet: Tablet still has lastMutationAt=100
Tablet->>Server: POST /v1/sync?lastMutationAt=100
Server-->>Tablet: 409 Conflict ❌
Note over Tablet: Must re-sync first!
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
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.
Here's the standard recovery pattern:
sequenceDiagram
participant Client
participant Server
Client->>Server: POST /v1/sync?lastMutationAt=T1
Server-->>Client: 409 Conflict
Note over Client: Start recovery...
Client->>Server: GET /v1/sync?mutationsSince=T1
Server-->>Client: { mutations: [...], lastMutationAt: T_server }
Client->>Client: Apply server changes locally
Client->>Client: Resolve any conflicts
Client->>Server: POST /v1/sync?lastMutationAt=T_server<br/>{ mutations: [...] }
Server-->>Client: 200 OK { lastMutationAt: T_new }
Note over Client: Sync complete ✓
- 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
For production-ready implementations, see the SDK docs.
When the same resource is modified both locally and on the server, you need a strategy:
Server changes always take precedence. Discard conflicting local changes.
Local changes always take precedence. Re-submit all local mutations.
Compare timestamps and keep the most recent change.
Combine changes field-by-field. Best for resources with independent fields.
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:
- 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
- Offline-First Patterns — Architecture for robust offline support