A customer taps "Pay now" on a slow mobile connection. The spinner hangs, they tap again, and a minute later they receive two payment confirmations. Nobody made a mistake on purpose, yet you now have a refund to process, an unhappy user and a support ticket. This is the classic duplicate request problem, and idempotency keys are the standard fix.
This guide explains what idempotency means in practice, how to implement it in a backend and a mobile client, and where teams usually go wrong. The numbers we use are illustrative examples, not reported statistics.
When your app sends a request and gets no response, it cannot know what happened. The request might never have arrived. It might have been processed, with only the response lost. It might still be running. The safe-looking option is to retry, but for operations that create something, retrying can duplicate it.
Duplicates come from many places, not only impatient users:
Because so many layers retry, you cannot fix this by asking the front end to be careful. The server has to be safe against repeats.
An idempotency key is a unique value, often a UUID, that the client generates for one logical action and sends with the request, usually in an Idempotency-Key header. The server remembers the key and the result. If the same key arrives again, the server returns the saved result instead of doing the work again.
The important detail is that the key represents the user's intent, not the network attempt. One tap on "Place order" creates one key. Every retry of that tap reuses it. A brand new order gets a new key.
Imagine an on-demand delivery app on patchy mobile data. In an illustrative scenario, a user places an order during the evening rush, the request reaches the server and the payment succeeds, but the response is lost in a tunnel. The app retries. Without protection, the second request creates another order and another charge.
With an idempotency key, the app generated the key when the user tapped the button and stored it locally with the pending order. The retry carries the same key. The server finds the key, sees the first request completed, and returns the same order confirmation. The user sees one order, the restaurant receives one ticket, and the payment provider sees one charge. If you are building this sort of product, our overview of on-demand delivery app development covers the surrounding architecture.
Your own API is only half the story. When you call a payment gateway, you need to pass an idempotency key to them too, and it must be derived from your own operation ID so that your retries reuse it. Otherwise your server can retry a gateway call after a timeout and create a second charge at their end even though your database only shows one order.
Apply the same idea to any external side effect: emails, SMS, shipping labels, and calls to other services. If you use several gateways, our article on payment orchestration across multiple gateways explains why a stable internal operation ID matters even more when a payment can be routed differently on retry.
Keys are not the only tool. Sometimes you can design operations that are naturally safe to repeat.
These layers stack. Keys protect the API boundary, constraints protect the database, and state machines protect the domain logic.
Decide whether to save failed responses. A validation error should be replayable, since the same input will fail again. A transient server error should usually release the key so the client can retry for real. Document which is which.
If the server dies after claiming a key but before finishing, the key is stuck in progress. Add a timeout on the in-progress state so a later request can take over safely, and make sure the underlying work is itself transactional.
Scope keys to the authenticated account so one user cannot collide with or read another's saved responses. Never accept guessable keys such as sequential numbers.
Idempotency is easy to believe in and easy to get wrong, so test it directly. Send the same request twice in parallel and confirm only one record exists. Kill the worker between steps and retry. Replay a request with a changed body. Simulate a lost response by dropping it at a proxy. These tests are cheap, and they catch the bugs that otherwise show up as real money problems. Our mobile app development team builds these cases into the checkout and booking flows we ship.
Retries are inevitable. Duplicates are optional.
For a typical backend with a handful of critical write endpoints, a small team can often add a reusable idempotency layer in a few days, since it is largely middleware plus one table. That is an illustrative estimate and will vary with your framework. The higher cost is retrofitting after launch, when duplicate data already exists and clients are already in the field, so building it in early is far cheaper.
The same problem appears outside HTTP. Most job queues guarantee at least once execution, which means a job can run twice if a worker crashes after finishing the work but before acknowledging it. Give each job a deterministic ID built from the business operation, for example "send-receipt-order-4821", and check whether that work has already been recorded before doing it. Emails are a good test case: a customer who receives the same receipt three times will notice, and a customer who receives none will notice more.
A useful habit is to write the side effect and the "already done" marker in one database transaction whenever the side effect lives in your own database. For effects outside your system, such as sending an email, record the intent first, perform the call with a provider level idempotency key when one exists, and record completion afterward. If the process dies in between, the retry picks up from the recorded intent instead of guessing.
Track how often a stored response is replayed. A sudden rise usually points to a client bug, a slow endpoint causing timeouts, or a network problem in one region. It is a cheap early warning signal, and it tells you where your latency is hurting real users before they complain.
Networks fail, users double tap and systems retry. Idempotency keys turn those inevitable events from expensive incidents into non-events. Generate the key at intent time, claim it atomically, save the response with the business change, pass keys through to external providers, and test the ugly cases. If you handle money or orders, this is one of the highest return reliability improvements you can make.