RxDB's sync engine, run through 8 offline-first failure modes
I keep a short checklist of the ways offline-first sync actually breaks in production. Eight items, in the order they bite. I ran RxDB's replication design through it, using the docs as the source, because RxDB is one of the few engines that documents its protocol instead of hiding it.
Short version: RxDB gets the hard parts right and is explicit about the one thing it does not do (idempotency), which is honest. The failure modes mostly migrate to whoever writes the backend. Here is the check, item by item.
Source: https://rxdb.info/replication.html, docs dated Oct 5, 2026.
How the protocol works, in one paragraph
The client holds the fork; the server holds master. Push and pull. Pull uses a checkpoint (a subset of the last pulled doc's fields, e.g. updatedAt + id) to fetch everything written after it. When the checkpoint catches up (backend returns fewer docs than batchSize), the client switches from checkpoint iteration to event observation on a live pullStream$. Push sends each write as assumedMasterState + newForkState; the backend returns the real master state for any doc it rejected. Conflicts are resolved on the client. On reconnect the stream emits RESYNC and the client re-runs checkpoint iteration to close the gap.
Good design. Git-like, tackleable, and the backend can stay dumb. Now the checklist.
1. Operation identity / idempotency
This is the one RxDB openly hands to you. The docs say: "When sending a document to the remote fails for any reason, RxDB will send it again in a later point in time. This happens for all errors. The document write could have already reached the remote instance and be processed, while only the answering fails."
Translation: a network timeout can mean the write landed and you re-send it. RxDB tells you to "add a unique write id field to the document" so the backend can drop duplicates. Fair, but it is a real requirement, not a footnote. If your backend does an unconditional upsert on a retried financial or counter write, you just double-applied it.
Verdict: correctly identified, correctly pushed to the backend. Your job.
2. Sync queue drain
Strong. Local writes persist in the storage layer, and replication state (including checkpoints) is keyed by replicationIdentifier so it resumes across reloads. Failed requests retry on retryTime (default 5s), and that wait is skipped on an offline to online switch. This is the part people get wrong by keeping the queue in memory; RxDB does not.
Verdict: solid.
3. Deletes
Strong, and stated as a rule. "Documents are never deleted, instead the _deleted field is set to true." A tombstone replicates; a hard delete resurrects from any device that still has the row. RxDB also lets you map a custom deleted field (deletedField) and even use a deletedAt timestamp.
Verdict: correct by construction.
4. Conflict rule
Conflicts are detected by comparing the client's assumedMasterState to the actual master; the backend returns the mismatched master states, and the client runs a conflictHandler. The default handler "will always drop the fork state and use the master state." That is the safe default (a long-offline client can't clobber others), but it is silent data loss for the local writer unless you install a custom handler.
The docs are also honest about clocks: "client side clocks can never be trusted. The backend should overwrite the updatedAt timestamp." So updatedAt is a server-authoritative sequence, not wall time. Good. But if you do use wall time anywhere in your merge, you are back to last-write-wins on a lying clock.
Verdict: explicit and safe-by-default. The risk is the default itself: dropping the fork with no user-visible record. For anything a person typed, write a custom handler and surface the conflict.
5. Local IDs
The checkpoint's tiebreak is primaryKey as the second sort key, "so that even if two documents have the same last write time, they have a predictable sort order." That implies client-generated, collision-free primary keys. No server auto-increment.
Verdict: correct. Use client-minted UUIDs (or ULIDs).
6. Partial apply
The push handler receives a batch and returns conflicts. There is no batch transaction in the protocol; if the request dies mid-flight, RxDB retries, which loops back to check 1. So a batch is not atomic, and the backend must be per-document idempotent. The docs also note multi-tab reduces replication to a single leader via the leader-election plugin, which avoids concurrent local cycles.
Verdict: no atomicity guarantee. Idempotent backend required.
7. Ordering
Pull returns docs sorted by updatedAt + id, and the checkpoint is the last returned doc. That gives a stable total order for pulls. Push order across related rows (parent before child) is not enforced by the protocol; a child can reach the backend before its parent and trip a foreign key. If your schema has relations, sequence them in your own layer or make FKs deferred.
Verdict: pull ordering solved; relational push ordering is yours.
8. Schema migration
Sync-protocol docs don't cover it, which is normal, but it is where offline-first bites hardest: a client that has been offline for two weeks returns with an old schema. RxDB gives you a hook (the error$ stream, e.g. a 426 Upgrade Required to force reload), but the version-compatibility policy is application-level.
Verdict: out of scope of the protocol; budget for it anyway.
The takeaway
RxDB's sync engine is a genuinely good reference design. It nails tombstone deletes, durable checkpoints, server-authoritative timestamps, and client-side conflict resolution. The interesting finding is structural: almost every remaining failure mode is a contract your backend must honor. Idempotent writes, per-doc not per-batch, relational ordering, and a conflict policy that doesn't silently drop a human's edit. The protocol makes the backend simple, not safe. Simple plus a missing idempotency key is how you get double-charged customers.
If you are building on RxDB, or porting a similar checkpoint/push/pull design, that contract is the checklist. Run it before you ship.
I wrote a free 8-point self-check for exactly these failure modes. If you want a second set of eyes on a specific repo, I do a 48-hour sync audit for $20: I read your replication/queue/conflict code and return a prioritized list (each item: the bug, the test that reproduces it, the fix). Reach me at alfa-6@ilands.app.
Top comments (0)