Background Mobile

Idempotent Sync for Apps That Lose Signal

backend development/
September 17, 2026
Idempotent Sync for Apps That Lose Signal

Offline-first and intermittent-connectivity apps fail in predictable ways. Here is how to design sync that survives signal loss without corrupting state or duplicating writes.

What Actually Goes Wrong When a Device Loses Signal Mid-Sync

Most mobile apps treat connectivity as binary: online or offline. The real world is messier. A device can send a write request, lose signal before receiving the acknowledgement, and then retry on reconnect. The server may have applied the write the first time. Now it applies it again.

For a counter increment, that means a double-count. For a financial transaction, it means a duplicate charge. For a record deletion, the retry might restore something that was already deleted.

The root cause is not the network drop. It is that the client has no way to distinguish "the server never received my request" from "the server received it, processed it, and the response got lost in transit." Both look the same from the client side: a timeout or a connection error.

Idempotency is the property that applying an operation once produces the same result as applying it N times. Building sync around idempotent operations shifts the question from "did the server process this?" to "what is the current agreed state?" That is a much safer question to design around.

How Idempotency Keys Actually Work

The canonical implementation is straightforward. The client generates a stable key for each logical operation, typically a UUID v4, before it sends the request. The server checks whether it has seen that key. If it has, it returns the previously recorded response without reprocessing. If it has not, it processes the operation and stores the key alongside the result.

Stripe has used this pattern publicly since at least 2015 and documents it in their API. The Idempotency-Key header is now a proposed IETF standard (draft-ietf-httpapi-idempotency-key-header). The mechanics are not new. What is underappreciated is where the implementation gets complicated.

Key generation and scope

The key must be stable across retries for the same logical operation, but different across distinct operations. A UUID generated fresh on each HTTP call is useless. The key needs to be generated once and persisted locally before the first attempt.

For queued operations on a device, a good pattern is to derive the key deterministically from the operation's content and context: a hash of (user ID + operation type + entity ID + client-side timestamp rounded to a session window). This avoids needing to persist a separate key store while still being collision-resistant for practical purposes.

Server-side key storage

You need to decide how long to retain idempotency keys. Stripe retains them for 24 hours. For long-lived sync queues (think field service apps where a technician might be offline for several days), you may need 7 to 30 days. The storage cost is low: you are storing a key, a response fingerprint, and a timestamp. A Redis sorted set with a TTL works well for this. PostgreSQL with a partial index on (idempotency_key, created_at) where processed = true is also fine and gives you durability without needing a separate cache layer.

The critical constraint: key lookup and response return must be atomic with the original write. If your server crashes between processing the write and persisting the key, you end up with an applied operation and no record of it. Use a database transaction that commits both together.

What Does the Client Queue Look Like in Practice?

A device sync queue is an ordered log of pending operations. Each entry has the idempotency key, the operation payload, retry metadata (attempt count, last attempt timestamp, backoff interval), and a status (pending, in-flight, confirmed, failed).

For React Native apps, SQLite via expo-sqlite or react-native-sqlite-storage is the standard local store for this queue. For Flutter, sqflite with a similar schema works. On iOS native, Core Data or GRDB over SQLite are reasonable choices. The important point is that the queue must be durable, not in-memory. A crash between retries should not lose the pending operation.

The sync loop on reconnect should:

  1. Query all pending operations ordered by creation time.
  2. For each operation, attempt the request with its stable idempotency key.
  3. On a 2xx response (including a "already processed" 200 from the server), mark the operation confirmed.
  4. On a 4xx response that is not 429 (rate limit), mark it failed and stop retrying. A 409 Conflict that carries the original response body should be treated as a success.
  5. On a 5xx or network error, apply exponential backoff with jitter. A good baseline is min(cap, base * 2^attempt) + random(0, 1000ms), with a cap around 30 seconds for most mobile use cases.

/// Not sure where to start?

Get the architecture before you commit

Tell us what you're building and we'll map the technical approach, stack, and rough timeline. No cost, no obligation, no sales call required.

Should You Conflict-Resolve on the Client or the Server?

This is the decision that actually determines how complex your sync layer becomes.

Client-side conflict resolution means the device merges state before sending. The server is authoritative only as a durable store. This works when operations are commutative (order does not matter) and associative (you can group them arbitrarily). Adding items to a set, incrementing a counter, appending to a log: these are good candidates. CRDTs (Conflict-free Replicated Data Types) formalise this. Libraries like Yjs (for document structures) and Automerge implement CRDTs in JavaScript and can be embedded in React Native or web apps.

Server-side conflict resolution means the device sends its operation and the server decides what to do, applying last-write-wins, version-vector comparison, or custom business logic. This is simpler for the client but requires the server to understand the semantics of every operation type. It also serialises writes, which becomes a bottleneck at scale.

Approach Good for Complexity lives in Risk
Client CRDT Collaborative editing, additive ops Client library Data model rigidity
Server last-write-wins Simple forms, preferences Server merge logic Stale writes overwriting newer data
Server version vectors Multi-device user data Both sides Implementation overhead
Operational transform Real-time text editing Server Hard to implement correctly

For most field-data apps (inspections, forms, inventory updates), server last-write-wins with a updated_at timestamp comparison is sufficient and the easiest to reason about. Do not reach for CRDTs unless you have a genuine collaborative editing requirement.

When Idempotent Sync Is the Wrong Choice

Not every app needs this. If your app is read-heavy and writes are rare, infrequent, and user-initiated (the user explicitly taps "save" in a moment when they have signal), a simpler optimistic UI with a retry toast is often good enough. The engineering overhead of a durable queue with idempotency keys is real. You are adding local schema management, migration logic, queue monitoring, and server-side key storage to your stack.

The pattern earns its keep when: writes happen continuously in the background (location traces, sensor readings, form autosave), the user cannot reasonably be expected to manually retry, duplicate writes have real consequences (financial records, compliance data), or the app is used in environments with sustained connectivity loss (logistics, field service, healthcare in clinical settings).

If only one of those is true for your app, evaluate carefully before committing.

Conclusion

Design your operations to be idempotent from the start, not retrofitted after you find a duplicate-write bug in production. Generate idempotency keys client-side before the first attempt, persist them locally, and store the server-side response atomically with the operation result. Pick your conflict resolution strategy based on your actual data semantics, not what sounds technically elegant.

If you are building or auditing a sync layer and want a second opinion on the architecture, get in touch with the Sodio team.

FAQ

What is the difference between idempotency and at-least-once delivery? At-least-once delivery is a guarantee from your transport or queue layer that a message will arrive, possibly multiple times. Idempotency is a property of your operation handler that makes duplicate delivery safe. You need both: the queue ensures delivery, the idempotency key ensures duplicate delivery does not corrupt state.

How long should I retain idempotency keys on the server? It depends on how long your clients might be offline before retrying. A 24-hour TTL covers most consumer mobile apps. Field service or logistics apps where devices can be offline for days need 7 to 30 days. Storage cost is negligible; the risk of a too-short TTL is duplicate processing after the key expires.

Can I use the same idempotency key for different operation types? No. Idempotency keys must be scoped to a single logical operation. If you reuse a key across operation types, the server may return a cached response from the wrong operation. Scope keys to at least (operation type, entity ID, session or time window).

What happens if the server processes an operation but crashes before storing the idempotency key? The next retry will be processed again as if it were a new request. This is why key storage must be transactional with the operation itself. Commit both in the same database transaction. If your architecture separates the write store from the key store (e.g., Postgres for data, Redis for keys), you need a two-phase commit or an outbox pattern to keep them consistent.

Are CRDTs worth the complexity for a standard mobile sync use case? Usually not. CRDTs solve a specific problem: concurrent edits to the same data from multiple clients where you want automatic, deterministic merge without a central authority. For most mobile apps where one user edits their own data across their own devices, a simpler last-write-wins or server-authoritative merge is easier to build, test, and debug. Reach for CRDTs when you have genuine multi-user collaborative editing.

Have a project in mind? Contact Sodio Technologies to discuss your requirements and explore the right technology solution for your business.

/// Work with us

Talk to the engineers who'd build it

You'll get a technical scope, timeline and cost estimate from the people doing the work, not an account manager. In-house team, no subcontracting, since 2016.

Contact Us