Idempotency Keys: Stop Double Charges and Duplicate Orders in Apps

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.

The Problem: Networks Lie

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.

What an Idempotency Key Actually Does

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.

Real-World Example: A Food Ordering App

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.

Step-by-Step: Implementing Idempotency

  1. Decide which endpoints need it. Mark every write endpoint that could be retried: payments, orders, bookings, refunds, invites, message sends.
  2. Generate the key on the client at intent time. Create a UUID when the user starts the action, and persist it with the pending action so it survives app restarts.
  3. Send it in a header. Use a consistent name such as Idempotency-Key across your whole API and document it clearly.
  4. Create a keys table. Store the key, the user or account ID, a hash of the request body, the status (in progress, completed, failed), the saved response code and body, and timestamps. Add a unique constraint on the key scoped to the account.
  5. Claim the key atomically. At the start of the request, insert the key row. If the insert fails because it exists, you know this is a repeat. The unique constraint does the locking for you, which is safer than checking first and inserting after.
  6. Handle in-progress repeats. If a second request arrives while the first is still running, return 409 or a "try again shortly" response instead of running both.
  7. Do the work and save the outcome in one transaction. Commit the business change and the stored response together, so you never have one without the other.
  8. Return the stored response on repeats. Send the same status code and body, so the client cannot tell the difference.
  9. Validate the body hash. If a key is reused with a different payload, reject it with a clear error.
  10. Expire old keys. Delete keys after a set window, for example 24 hours to 7 days, with a scheduled cleanup job.

Calling External Providers Safely

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.

Natural Idempotency and Other Techniques

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.

Edge Cases Worth Planning For

Failures and what to cache

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.

Crashes mid-request

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.

Key scope and security

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.

Testing It Properly

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.

Key Benefits

Retries are inevitable. Duplicates are optional.

How Much Effort Is It?

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.

Idempotency in Background Jobs and Queues

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.

Monitoring Duplicate Attempts

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.

Conclusion

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.

Frequently Asked Questions

What does idempotent mean in API design?
An operation is idempotent if performing it several times has the same effect as performing it once. Idempotency keys let normally unsafe operations, like creating a payment, behave that way.
Which requests need idempotency keys?
Any request that creates or changes something and might be retried: payments, orders, bookings, transfers, sign ups and message sends. GET requests are already safe, and PUT and DELETE are usually idempotent by design.
How long should idempotency keys be stored?
Long enough to cover the longest realistic retry window, commonly 24 hours to several days. Storing them for a fixed period, then expiring them, keeps the table small.
What if the same key is reused with a different request body?
Reject it with a clear error such as 422 or 409. Reusing a key for a different payload is almost always a client bug, and silently accepting it can hide serious mistakes.
Do idempotency keys work with mobile apps on poor networks?
Yes, they are especially useful there. The app generates a key when the user taps the button and reuses it on every retry, so a timeout followed by a retry cannot create a second order.