Every backend engineer eventually meets the same bug: a request runs twice and something happens twice that should only happen once. A card gets charged, an email goes out, a row is inserted. The cause is almost never your code being "wrong" — it is the network doing exactly what it is allowed to do.
Why duplicates are inevitable
A client sends a request and the connection drops before the response comes back. Did it succeed? The client cannot know, so a well-behaved client retries. Add load balancers, message queues with at-least-once delivery, and impatient users clicking twice, and duplicates stop being an edge case — they are the steady state.
The fix is not to prevent retries. It is to make a repeated request produce the same result as a single one. That property is called idempotency.
The idempotency key pattern
The most reliable approach is to let the client send a unique key with each logical operation:
POST /payments
Idempotency-Key: 9f1c2b7a-4e21-4c8e-9a0c-3b7d5e2f1a88
{ "amount": 4999, "currency": "INR" }
On the server:
- Look up the key in a store (a table, Redis — anything durable).
- Seen before? Return the saved response. Do no work.
- New? Do the work in a transaction, save the response against the key, then return it.
The key turns "at least once" delivery into "effectively once" behaviour, which is what your users actually care about.
Three rules that keep it honest
- Scope keys to an operation, not a session. One key = one intended action.
- Store the response, not just a flag. A retry should get the original result, not a fresh 200 with no body.
- Make the write and the key-save atomic. If they can drift apart, you have just moved the bug somewhere harder to find.
Don't forget the verbs you get for free
GET, PUT, and DELETE are idempotent by definition when you implement them faithfully. PUT /users/42 with the same body, run ten times, should leave one user in one state. Lean on that — reserve the idempotency-key machinery for the genuinely unsafe operations like POST.
Idempotency is not a feature you bolt on after an incident. It is a default you design in, so the incident never gets written.