A React Native warehouse app replays queued scans after reconnecting, and the server records some scans twice; why, and how do idempotency keys fix it?
answer
- committed on server, response lost
- killed between success and dequeue
- overlapping replay triggers
- key generated at enqueue, not send
- server returns the stored result
basics
~20 sScans repeat when the server commits but the response is lost, the app dies before dequeuing, or replays overlap. An idempotency key created when the scan is queued lets the server recognise repeats and return the first result.
solid answer
~50 sDuplicates come from the gap between the server committing a write and the client knowing it did. A response lost as the picker leaves Wi-Fi, or the app being killed after the server accepted the scan but before the record was removed, both leave a committed scan in the outbox, and the next replay sends it again. Overlapping triggers, such as a NetInfo change and a return to `active`, can also send the same record twice without a single-flight guard. The client cannot rule out the first two, so the write must be safe to repeat: generate a unique key when the scan is queued, store it in the outbox record, send it on every attempt, and have the server store it with the result and return that result for repeats, which the client treats as success.
code
typescript · 20 linesimport type { OutboxItem } from './outboxStore';
const API_URL = 'https://api.example.com';
// item.id was generated when the scan was queued and never changes.
export async function sendScan(item: OutboxItem): Promise<'accepted' | 'rejected'> {
const res = await fetch(`${API_URL}/scans`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Idempotency-Key': item.id,
},
body: JSON.stringify(item.payload),
});
if (res.ok) return 'accepted'; // includes a repeat answered with the stored result
if (res.status === 408 || res.status === 429 || res.status >= 500) {
throw new Error(`Transient failure ${res.status}`); // keep queued
}
return 'rejected'; // other 4xx: set aside and show the user
}go deeper
Know that a write can succeed on the server even when the app sees an error, so replaying a queue can create duplicates unless each write carries a stable key.
Explain the four sources of duplicates, and why the key is generated at enqueue time and stored with the record rather than created per request.
Show the full contract: stable key per logical write, single-flight ordered replay, a server that returns the stored result for a repeat, and status handling that separates transient failures from rejections.
Push for idempotent write endpoints as a platform standard, since every offline client and every retrying service depends on the server recognising repeats.
## The symptom The warehouse app queues scans while the picker is in a dead zone and replays them when the phone reconnects. The server's order counts drift upwards: some scans are recorded twice. The outbox code looks correct, since each record is removed after a successful response. The duplicates come from the gap between **the server committing a write** and **the client learning that it did**. ## Where duplicates come from 1. **Lost responses.** The request reaches the server and is committed, but the response never makes it back: the picker walks out of Wi-Fi range mid-request, or the request times out on a weak signal. The client sees a network error, keeps the record, and sends it again on the next trigger. 2. **Death between commit and dequeue.** The server accepts the write, and the OS kills the app before the record is removed from local storage. At the next launch, replay sends it again. 3. **Overlapping replays.** The NetInfo listener fires when you subscribe and again on each change, the app returns to `active`, and the picker taps "sync now". Without a guard, two replay loops send the same head record concurrently. 4. **A fresh id per attempt.** If the client generates the request's unique id inside the send function, every retry looks like a new write to the server. Causes 1 and 2 cannot be removed on the client at all: over an unreliable network, the client can never be sure whether a request whose response it did not receive was applied. The only complete fix is to make the write **safe to repeat**. ## The fix: an idempotency key per queued write - **Generate the key when the write is queued**, not when it is sent, and store it in the outbox record. Every attempt of that write carries the same key. - **Send it on every attempt**, usually in a request header (a header named `Idempotency-Key` is a common convention) or as a client-generated id in the body. - **The server stores the key with the result** of the first successful attempt. A later request with the same key does not apply the write again; it returns the stored result. - **The client treats that repeated answer as success** and removes the record. The key must be unique per logical write, so a random UUID generated at enqueue time is the usual choice. Deriving it from the payload alone is wrong here: a picker legitimately scanning the same SKU twice for one order would be collapsed into one scan. ## Client rules that go with the key | Rule | What it prevents | |---|---| | Key generated at enqueue, stored in the record | A new key per retry defeating deduplication | | Single-flight replay | Two loops sending the same record concurrently | | Remove the record only after a success or a "seen this key" answer | Losing a write the server never received | | Replay oldest first, one at a time | A later scan overtaking an earlier one | | Retry network errors, 5xx, 408 and 429; set aside other 4xx | A poison record blocking the queue | With TanStack Query as the queue, the same idea applies: put the key in the mutation's **variables** when the user acts (the variables are what a persisted paused mutation carries), not inside `mutationFn`, which runs again on every attempt. ## Testing it on a real phone Duplicates rarely appear on a simulator with a stable connection, so reproduce the conditions deliberately. Throttle or cut the network in the middle of a request (walk out of Wi-Fi range, or use the platform's network conditioning tools) and confirm the record is retried with the same key. Kill the app from the task switcher right after a scan is sent, relaunch, and check that the server holds one scan, not two. Trigger a reconnect and a return to the foreground together and check that only one replay loop runs. Each test maps to one source of duplicates above. ## What the key does not solve Idempotency makes one write safe to repeat. It does not decide what happens when two devices edit the same order while offline, or when the server's state has moved on since the scan was queued. That is conflict resolution, a separate design question. The key answers the narrower, very common interview question: why the replay of an offline queue produced duplicates, and why retrying more carefully on the client alone can never fully fix it.
- Why is an idempotency key derived from the scan's SKU and order id a bad choice for this React Native warehouse app?A picker can legitimately scan the same SKU twice for one order, for example when the order needs two units. A key derived from the payload would make the server treat the second real scan as a repeat of the first and drop it. The key must identify one logical write, so generate a random id when the scan is queued.
- With TanStack Query as the offline queue in React Native, where should the idempotency key be created?In the mutation's variables, when the user acts: `mutate({ ...scan, idempotencyKey: newId() })`. Variables are what a paused mutation keeps and what a persisted one restores, so every attempt reuses the key. Generating it inside `mutationFn` would produce a new key on each attempt and defeat deduplication.
saying these in an interview costs you the question
- Removing the record right after a successful response makes duplicates impossible.
- A network error means the server never received the request.
- Generating a new request id on each retry is enough for deduplication.
- The idempotency key can be a hash of the payload.
- Duplicates are a server bug, so the client needs no changes.