# Sync Strategies

> Mobile sync strategies - pull, push, delta, bidirectional, event log, and CRDT - with tradeoffs, payload shapes, and cross-platform snippets. Use when designing how a mobile client stays consistent with a server.

- Skill: `almasumdev/sync-strategies` (Agent Skill)
- Install (CLI): `npx skillmds@latest add almasumdev/sync-strategies`
- Raw SKILL.md: https://api.skillmd.com/api/skills/almasumdev/sync-strategies/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: almasumdev (https://skillmd.com/u/almasumdev)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/almasumdev/sync-strategies

---


# Sync Strategies

## Instructions

Sync is the hardest part of offline-first. Pick the simplest strategy that meets the product's consistency needs. Do not default to CRDTs because they are fashionable.

### 1. Strategy Menu

| Strategy | Direction | Good for | Cost |
| --- | --- | --- | --- |
| Pull (poll) | Server -> client | Read-heavy, infrequent changes | Latency, wasted requests |
| Push (server-initiated) | Server -> client | Real-time notifications | Infrastructure (APNs/FCM/WS) |
| Delta sync | Server -> client | Large datasets with incremental updates | Requires server cursors |
| Outbox push | Client -> server | Writes while offline | Ordering and retry logic |
| Bidirectional | Both | Collaborative features | Conflict resolution |
| Event log / CRDT | Both, merge-friendly | Collaborative + offline + conflict-heavy | Engineering complexity, payload size |

Most apps need **pull + outbox**. A subset need **delta**. Real-time collab needs **bidirectional**. CRDTs are reserved for cases where conflicts are the norm.

### 2. Pull Sync

Simple and robust. The client asks for "things changed since X".

```http
GET /articles?updatedSince=2026-04-19T12:00:00Z&limit=200&cursor=abc
```

- Use a monotonic cursor (opaque) rather than a timestamp when clock skew matters.
- Cap page size; iterate until empty.
- Store the cursor per entity kind on the device.
- Revalidate only what is visible or recently used; full-table pulls are rare.

### 3. Push Sync

Server tells the client when to sync.

- Transport: silent push (APNs/FCM data), websocket, SSE.
- Payload contains the minimal hint (`{type:"sync", entity:"articles"}`), not the full change.
- The client then performs a delta pull.
- Silent push is throttled; always fall back to periodic pull.

### 4. Delta Sync

When the dataset is large, send only what changed.

```json
// server response
{
  "changes": [
    { "op": "upsert", "entity": "article", "id": "a1", "rev": 12, "body": {...} },
    { "op": "delete", "entity": "article", "id": "a2", "rev": 3 }
  ],
  "nextCursor": "abc123",
  "hasMore": true
}
```

- The server assigns monotonically increasing revisions per entity.
- The client applies ops in order within a transaction.
- Use batching (typically 100-500 ops per page) so UI updates are perceptible, not jittery.

### 5. Outbox Push

Writes go to a local outbox:

```sql
CREATE TABLE outbox(
  id TEXT PRIMARY KEY,
  created_at INTEGER NOT NULL,
  entity TEXT NOT NULL,
  op TEXT NOT NULL CHECK (op IN ('create','update','delete')),
  payload BLOB NOT NULL,
  attempts INTEGER NOT NULL DEFAULT 0,
  last_error TEXT
);
```

- Idempotent ops. Each outbox row has a client-generated UUID; the server dedupes on retry.
- Exponential backoff with jitter; cap attempts; park-and-surface after the cap.
- Drain on app foreground, connectivity regained, or periodic background work.
- Never reorder within a single entity's history; between entities, parallelism is fine.

```kotlin
suspend fun drainOutbox() {
    outboxDao.pending().forEach { row ->
        try {
            api.apply(row)                      // idempotent via row.id
            outboxDao.delete(row.id)
        } catch (e: RetryableException) {
            outboxDao.markFailure(row.id, e.message)
        } catch (e: PermanentException) {
            conflictDao.record(row, e)          // surface to UI
            outboxDao.delete(row.id)
        }
    }
}
```

### 6. Bidirectional Sync

Combine delta pull with outbox push. Core concerns:

- **Versioning**: each entity has a server revision. The client sends `baseRev` with writes.
- **Conflict**: if the server has moved on, it rejects or returns a conflict payload.
- **Resolution**: see `conflict-resolution` skill.
- **Eventual consistency**: the UI must tolerate a brief window where client and server disagree, marked as "pending" or "syncing".

### 7. Event Log / CRDT

Use when multiple devices edit the same document and conflicts cannot be avoided.

- **Event log**: client sends append-only events with a logical clock; server orders them; all clients rebuild state by replay.
- **CRDT**: conflict-free replicated data types (RGA for text, OR-Set for sets). Libraries: Yjs, Automerge, Fluid.

Cost: payloads are larger, merges are probabilistic in ordering, and debugging is harder. Justify with product need.

### 8. Scheduling

- **On foreground**: revalidate visible screens, drain outbox.
- **On push**: revalidate the hinted entity.
- **Periodic**: `WorkManager` on Android (`PeriodicWorkRequest`), `BGAppRefreshTask` on iOS. Keep to minutes-hour cadence.
- **On connectivity regained**: drain outbox, not a full refresh.

### 9. Observability

Track:

- Outbox length, age of oldest pending write, failures by class.
- Delta pull duration and page count.
- Conflict counts per entity.
- Sync battery cost via platform tools (Battery Historian, Instruments).

### 10. Anti-Patterns

- Polling every few seconds while in foreground. Use push or longer intervals.
- Full-table pulls on every launch.
- Writing directly to the network without an outbox.
- Ordering guarantees that cross entities without justification.
- Relying solely on silent push for reliability.
- Adding CRDTs before proving conflicts are frequent.

## Checklist

- [ ] A sync strategy is chosen and documented per entity kind.
- [ ] Cursors/revisions are monotonic and opaque.
- [ ] Outbox writes are idempotent with client-generated IDs.
- [ ] Backoff with jitter; failures are surfaced after the cap.
- [ ] Drain triggers cover foreground, push, periodic, and connectivity regained.
- [ ] Delta pages are sized to keep UI responsive.
- [ ] CRDTs are used only when conflicts are frequent and justified.
- [ ] Sync metrics (outbox length, conflict rate, pull duration) are tracked.

