A user taps “Pay” on a checkout screen. The request reaches your server, the payment is processed, and then the connection drops before the response gets back. The app sees a timeout and retries. Does the customer get charged twice?
The answer depends entirely on whether your API is idempotent. This is one of those backend concepts that sounds abstract until it costs someone real money — and then it becomes the most important property your endpoints have.
What idempotency means
An operation is idempotent if performing it multiple times produces the same result as performing it once. The first call may change something; every identical call after that changes nothing further.
A light switch that toggles is not idempotent — pressing it twice returns you to the original state. A switch that sets the light to “on” is idempotent — no matter how many times you set it to on, the light ends up on.
Note that idempotency is about the final state of the system, not about the response being byte-for-byte identical. Deleting a resource that is already deleted may legitimately return a different status code the second time; what matters is that the resource is gone either way and nothing else was damaged.
HTTP methods and their guarantees
| Method | Safe (no changes) | Idempotent | Typical use |
|---|---|---|---|
| GET | Yes | Yes | Read a resource |
| HEAD | Yes | Yes | Read headers only |
| PUT | No | Yes | Replace a resource entirely |
| DELETE | No | Yes | Remove a resource |
| POST | No | No | Create a resource, trigger an action |
| PATCH | No | Not guaranteed | Partially update a resource |
Two clarifications are worth making. First, PATCH is not idempotent by default. A patch that says “set status to approved” is idempotent; one that says “increment the counter by one” is not. The method does not decide this — your implementation does.
Second, these are specification guarantees, not automatic behaviour. If your PUT handler appends to a list instead of replacing it, the method is idempotent in name only. The contract is something you have to honour in code.
Why POST is the problem
POST exists precisely for operations that should happen once and produce something new each time: create an order, send an email, charge a card. That is exactly what makes retries dangerous.
And retries are not optional in distributed systems. Mobile networks drop. Load balancers time out. Client libraries retry automatically. Message queues redeliver. You cannot prevent a request from arriving twice — you can only decide what happens when it does.
Idempotency keys
The standard solution is an idempotency key: a unique identifier that the client generates and sends with the request, usually in a header. The flow works like this:
- Before sending, the client generates a unique value — typically a UUID — and attaches it to the request.
- The server checks whether it has already seen that key.
- If not, it processes the request, stores the key together with the resulting response, and replies normally.
- If it has seen the key, it skips processing entirely and returns the stored response.
- The client can retry as many times as it likes; only the first attempt has any effect.
The critical detail is that the client generates the key, not the server. If the server generated it, each retry would receive a new one and the deduplication would never trigger. The key must stay constant across all retries of the same logical operation.
Implementation details that matter
- Store the key atomically with the work. If you record the key in one transaction and perform the operation in another, a crash between them leaves the system in an inconsistent state. Where possible, use a unique database constraint so a duplicate insert fails cleanly.
- Handle concurrent duplicates. Two identical requests can arrive at the same moment on different servers. A unique index on the key, or a short-lived lock, prevents both from processing.
- Cache the response, not just the key. The retrying client needs an answer. Returning an empty success is not equivalent to returning the order ID it expected.
- Set a retention window. Keys do not need to live forever. Twenty-four hours is a common choice; anything longer bloats storage without practical benefit.
- Reject key reuse with different payloads. If the same key arrives with a different request body, that is a client bug. Returning an error is safer than silently serving a mismatched cached response.
Idempotency beyond HTTP
The same reasoning applies wherever messages can be delivered more than once. Most message brokers guarantee “at least once” delivery, which means duplicates are expected rather than exceptional. Consumers therefore need to be idempotent too — usually by recording the identifier of each processed message and skipping repeats.
Database migrations, scheduled jobs and webhook handlers all benefit from the same discipline. A webhook provider that does not receive a success response will send the event again; if your handler creates a record every time, you will end up with duplicates in production.
A quick design checklist
- Ask, for every endpoint that changes state: what happens if this runs twice?
- Use PUT rather than POST when the client can determine the resource identifier.
- Require idempotency keys on any operation involving money, messaging or external side effects.
- Make retries explicit and bounded on the client, with backoff between attempts.
- Write a test that sends the same request twice and asserts a single effect. It is the cheapest safeguard available.
Conclusion
Idempotency is what makes retries safe, and retries are unavoidable. Designing endpoints so that repeating them is harmless turns an entire class of race conditions and duplicate-charge bugs into non-events. It costs a little extra thought at design time and saves a great deal of investigation later.
If you want to go deeper into API design, error handling and distributed system patterns, the free backend development and programming courses on Cursa are a good place to continue.



























