Idempotency Keys: Prevent Duplicate Orders and Double Payments

Idempotency Keys: Prevent Duplicate Orders and Double Payments — cover image

The bug that charges customers twice

A customer taps Pay Now on a patchy mobile connection. The screen freezes. They tap again. Behind the scenes, your server received both requests, and both succeeded. The customer is charged twice, your support inbox lights up, and trust takes a hit that no discount code will repair.

This is not a rare edge case. Networks drop responses, mobile apps retry, load balancers time out, and users double click. Any system that handles orders, payments, or bookings has to assume that the same request will arrive more than once. The standard defence is the idempotency key, and it is one of the highest value patterns in backend design.

This guide explains what idempotency keys are, how to implement them safely, and where startups most often get them wrong.

What idempotency means in plain language

An operation is idempotent when doing it many times has the same effect as doing it once. Setting a user's email to a new address is idempotent. Creating an order is not, because every call makes another order unless you guard against it.

An idempotency key is a unique token the client attaches to a request. The server remembers the key and the result. If the same key shows up again, the server skips the work and returns the stored result. The customer sees one charge, one order, and one confirmation email.

A realistic example: the checkout retry

Imagine an e-commerce startup with a mobile app. The app calls the order endpoint, the server creates the order and charges the card, but the response is lost because the user walked into a lift. The app times out and retries. Without a key, the server creates a second order. With a key, the server recognises the repeat, finds the first order, and returns it, so the customer ends up with exactly one.

The same pattern protects refunds, wallet top ups, subscription renewals, and invoice creation. Integrations with Indian payment providers are especially exposed, because webhooks and callbacks can arrive late or twice. Our guide on choosing between Razorpay, Cashfree, and Stripe in India covers the provider side, and idempotency is how you keep your own side consistent with them.

Why this matters more than it seems

How an idempotency key works, step by step

  1. The client creates a key. When the user begins an action, generate a random UUID and keep it for that action.
  2. The client sends it in a header. A common convention is an Idempotency-Key header on the POST request.
  3. The server looks up the key. Before doing any work, check a table for a record with that key and that account.
  4. If it is new, claim it. Insert a record with status in progress, using a unique constraint so two simultaneous requests cannot both claim it.
  5. Do the work. Create the order, call the payment provider, and write results in a database transaction where possible.
  6. Store the response. Save the status code and body against the key, and mark it complete.
  7. Replay on repeat. If the same key arrives again, return the saved response without repeating the work.
  8. Expire old keys. Delete keys after a window such as a day or several days.

The design details that make or break it

Use a database constraint, not just a check

The most common flaw is a check-then-insert sequence in application code. Two identical requests arriving milliseconds apart can both pass the check. Put a unique constraint on the key (scoped to the account) so the database itself rejects the second claim. Treat that rejection as the signal to wait for or replay the first result.

Handle the in progress state

What if the second request arrives while the first is still running? Return a 409 Conflict or ask the client to retry shortly. Never start a second copy of the work. Mobile clients should treat this as a polite signal to wait and try again with the same key.

Bind the key to the request

Store a hash of the request body along with the key. If the same key arrives with a different body, reject it. This catches client bugs such as reusing one key for every purchase, which would otherwise return the wrong order to the wrong cart.

Scope keys per customer

Keys should be unique within an account or user, not globally. Otherwise one customer's key could collide with another's, or worse, expose someone else's stored response.

Make downstream calls idempotent too

Your server may call a payment provider, an SMS gateway, or an accounting system. Pass your own key or a derived reference to them where they support it. Otherwise a retry on your side could duplicate the action on theirs. For UPI flows, our UPI payment integration guide explains why reconciliation matters even when the app looks correct.

Where idempotency fits with queues and retries

Background jobs add a second source of duplicates. Queues often guarantee at-least-once delivery, which means a job can run twice. Each job handler should be idempotent in the same way, usually by checking a unique business identifier before acting. Our overview of background jobs and queues for web apps explains delivery guarantees in more detail.

Inbound webhooks follow the same rule. Providers may deliver the same event more than once, so store the event ID and ignore repeats. A processed events table with a unique constraint is often all you need.

Common mistakes to avoid

Testing it properly

Idempotency bugs hide until real traffic arrives, so test them on purpose. Fire the same request twice in quick succession and confirm only one order exists. Fire it twenty times in parallel. Kill the server after the charge but before the response is saved and confirm the retry recovers cleanly. Simulate a slow payment provider and a dropped connection. These tests take an afternoon to write and can prevent a very expensive incident.

Pair the tests with solid monitoring. A dashboard that shows how often keys are replayed can reveal flaky client networks or buggy releases early. For wider reliability thinking, see our guide to error handling in reliable automation workflows.

A simple data model to start with

A small table is enough: the account ID, the key, a hash of the request, the status (in progress or complete), the saved response code and body, and created and expiry timestamps. Add a unique index on the account ID and key together. That single index does most of the heavy lifting.

Illustratively, a team might wrap this logic in a reusable middleware so every create endpoint gets protection with one line of configuration, instead of each developer reinventing it under deadline.

What the client should do

Server logic is only half the story. The client must generate the key once per user intent and keep it until the action is confirmed. In a mobile app, that often means saving the key with the pending order in local storage, so that if the app is killed and reopened, the retry uses the same key. Without that, a restart quietly creates a new identity for the same purchase and the protection disappears.

Clients should also retry sensibly. Use exponential backoff with a little random jitter, cap the number of attempts, and show the user a calm status such as Confirming your payment rather than an alarming error. When the final answer is uncertain, offer a way to check order status instead of encouraging another purchase attempt.

Finally, document the behaviour for partners and your own frontend developers. State which endpoints accept keys, how long they are valid, and which status codes mean replay, conflict, or mismatch. Clear contracts turn a clever trick into a dependable platform feature.

Key benefits of adopting idempotency keys

Getting expert help

Idempotency touches your database design, API contracts, mobile clients, and third-party integrations, so it is best designed up front rather than patched in. If you are building a marketplace, fintech, or e-commerce product, our web development services include payment-safe backend design from the first release.

Conclusion

Networks fail, users double tap, and servers retry. A well built system expects all of it. Idempotency keys give every important action a unique identity, so repeating a request is harmless. Add them to your order, payment, refund, and booking endpoints, back them with a database constraint, bind each key to its request, and test the ugly cases deliberately.

The investment is small, and the payoff is a checkout that behaves correctly even on the worst connection. Your customers will never notice the work, which is exactly the point.

Frequently Asked Questions

What does idempotent mean in an API?
An idempotent operation produces the same result no matter how many times it is repeated. If a client sends the same request twice, the server performs the action once and returns the same response the second time.
Which endpoints need idempotency keys?
Any POST that creates something or moves money: orders, payments, refunds, invoices, bookings, and message sends. GET, PUT, and DELETE are usually idempotent by design, so they rarely need keys.
How long should an idempotency key be stored?
Many teams keep keys for 24 hours to a few days, long enough to cover client retries and network recovery. Choose a window that matches how long a client could realistically retry the same action.
Who generates the idempotency key?
The client should generate it, usually a random UUID created when the user starts the action. The same key is reused for every retry of that action, and a new key is used for a genuinely new action.
What if the same key arrives with a different request body?
Reject it with a clear 4xx error such as 422 or 409. A key must always map to one exact request, and a mismatch usually signals a client bug that should be surfaced rather than silently accepted.