A user clicks Pay, Submit, or Place Order once. But from the server's point of view, that request might arrive twice.
The first request could succeed while the client waits too long for a response. The user then clicks the button again. A mobile network may retry the request automatically. A reverse proxy might repeat a request after a connection failure.
Now the backend has to answer an important question:
How do we make sure one user action does not accidentally become two?
This is where idempotency becomes important.
Idempotent APIs allow clients to safely retry certain operations without creating duplicate side effects. The concept is especially useful for applications that handle payments, orders, bookings, account changes, or other operations where repeating an action can cause real problems.
In this article, we will look at how idempotent APIs work, why retries are difficult, and how developers can design safer request flows.
What Does Idempotency Mean?
An operation is idempotent when performing it multiple times produces the same final effect as performing it once.
For example, consider an API that updates a user's profile:
PUT /api/users/123
If the client sends the same update three times, the final profile should still contain the same information.
That is relatively straightforward.
The more difficult case is an operation that creates something:
POST /api/orders
If the client sends this request twice, the server could create two orders.
That may happen even when the user only intended to create one.
The goal of an idempotent design is to allow the client to retry without accidentally duplicating the operation.
Why Duplicate Requests Happen
Duplicate requests are not always caused by careless users.
They can happen for several reasons:
- A user double-clicks a button.
- A mobile connection becomes unstable.
- The client times out before receiving the response.
- A reverse proxy retries a request.
- A background worker processes the same message twice.
- A frontend retries after assuming a request failed.
- A server completes the operation but the response never reaches the client.
Consider this sequence:
Client
|
| POST /api/payment
|
v
Server
|
| Payment succeeds
|
X Response lost
The client does not know whether the payment succeeded.
It may retry:
Client
|
| POST /api/payment
|
v
Server
Without an idempotency mechanism, the backend might process the operation again.
The difficult part is that the first operation actually succeeded.
Use an Idempotency Key
One common solution is an idempotency key.
The client generates a unique identifier for a particular operation and sends it with the request.
For example:
POST /api/orders
Idempotency-Key: 8f4c1e8a-2f44-4f6d-a3c1-91d9d1c7b201
The server stores the key when it processes the request.
If another request arrives with the same key, the server knows it is probably a retry of the same operation.
A simplified flow looks like this:
Request arrives
|
v
Check idempotency key
|
+---- Already processed? ----> Return previous result
|
|
v
Process operation
|
v
Store result + key
|
v
Return response
The key should represent the operation, not simply the user.
For example, a user might legitimately place two different orders. Those orders need different idempotency keys.
Store More Than the Key
Simply recording that a key has been seen is often not enough.
Suppose the first request succeeds but the response is lost.
The client sends the same request again.
The server finds the existing key.
What should it return?
Ideally, it should return the result of the original operation.
A record might therefore contain:
idempotency_key
request_hash
status
response_code
response_body
created_at
The exact fields depend on the application.
The important idea is that the server can reconstruct the result instead of executing the operation again.
Validate Reused Keys
There is another subtle problem.
Imagine a client sends:
Idempotency-Key: abc123
with one request.
Later, it accidentally uses the same key for a completely different request.
The server should not silently treat the second request as the original operation.
One useful approach is to associate the key with a hash of the relevant request data.
Conceptually:
key = abc123
request_hash = SHA256(request data)
When the same key arrives again, compare the request hash.
If the key exists but the request data is different, return an error rather than processing it as a retry.
This prevents accidental key reuse from producing confusing behaviour.
Make the Database Part of the Design
Idempotency cannot depend only on application memory.
Consider this implementation:
if (processedKeys.has(key)) {
return previousResponse;
}
processedKeys.add(key);
processRequest();
It might appear to work.
But what happens when you have multiple application servers?
Load Balancer
/ \
/ \
Server A Server B
The first request may reach Server A.
The retry may reach Server B.
If each server has its own memory, Server B does not know that Server A already processed the request.
For distributed applications, idempotency information normally needs to live in shared infrastructure such as a database or distributed cache.
Avoid the Race Condition
There is another problem that developers often miss.
Two identical requests can arrive almost simultaneously:
Request A ----\
>---- Server
Request B ----/
Both requests check the database.
Both see:
Key does not exist
Both continue processing.
You have now lost the benefit of the idempotency check.
The database should therefore enforce uniqueness where appropriate.
For example:
CREATE UNIQUE INDEX unique_idempotency_key
ON idempotency_records (idempotency_key);
The exact implementation depends on the database and architecture, but the principle is important:
Do not rely on a check-then-insert sequence without protecting the operation against concurrent requests.
Idempotency and Transactions
For operations involving multiple changes, database transactions become particularly useful.
Imagine an order operation that needs to:
- Create an order.
- Record a payment.
- Update account information.
If step one succeeds and step two fails, the system could be left in an inconsistent state.
A transaction can help group operations that must succeed or fail together.
A simplified example:
BEGIN TRANSACTION
Create order
Record payment
Update account
COMMIT
If something goes wrong:
ROLLBACK
Idempotency and transactions solve related but different problems.
Transactions help maintain consistency within a unit of work.
Idempotency helps make retries safe.
Reliable systems often need both.
What About GET, PUT and DELETE?
HTTP methods have different semantics, and developers should understand those semantics when designing APIs.
A GET request should normally be safe to repeat because it reads data rather than creating a new side effect.
PUT is generally designed around replacing or creating a resource at a known location, making repeated identical requests predictable.
DELETE is also commonly designed so that repeating the request does not keep deleting additional resources.
POST is more complicated because it is commonly used for operations that create new resources or trigger actions.
That does not mean every POST must be non-idempotent. It means developers need to deliberately design the behaviour.
Retries Need Limits
Idempotency makes retries safer, but it does not mean clients should retry forever.
A good retry strategy should consider:
- Maximum retry attempts
- Exponential backoff
- Network timeouts
- Server response codes
- Retryable versus non-retryable errors
- Maximum request lifetime
For example:
Attempt 1
|
| failure
v
Wait 500ms
|
Attempt 2
|
| failure
v
Wait 1s
|
Attempt 3
Exponential backoff reduces the chance that thousands of clients will repeatedly hit an already struggling server.
Logging Makes Debugging Easier
When something goes wrong in a distributed system, request IDs become extremely valuable.
For example:
request_id: req_9182
idempotency_key: idem_4421
user_id: user_781
status: completed
A developer can then follow the operation across application logs, database records, queues, and downstream services.
This is particularly useful in systems where users expect immediate feedback, including transaction-heavy applications and live platforms such as sports applications.
For example, when interacting with a platform such as Goka, a reliable backend architecture matters because users expect actions submitted through an application to produce predictable results even when networks are unreliable.
The important engineering lesson is not the particular product. It is that user actions should have clear server-side identities and predictable retry behaviour.
Idempotency Is Not a Complete Reliability Strategy
It is tempting to think that adding an Idempotency-Key header solves everything.
It does not.
Reliable applications also need to consider:
- Database consistency
- Authentication
- Authorisation
- Rate limiting
- Timeouts
- Distributed locks where appropriate
- Message delivery
- Queue retries
- Observability
- Error handling
- Data validation
Idempotency is one part of a larger reliability strategy.
A Practical Checklist
Before shipping an API that performs an important operation, ask:
- Can the request be safely retried?
- What happens if the client times out after the server completes the operation?
- Does the API accept an idempotency key?
- Where are idempotency records stored?
- Is the key protected against race conditions?
- Can the same key be reused with different request data?
- Can the original response be returned?
- Are retryable errors clearly defined?
- Does the database transaction protect related changes?
- Can developers trace the request through logs?
If you cannot answer these questions, the API may behave unpredictably when the network does.
Final Thoughts
Reliable APIs are not only about returning a successful response when everything works.
They are also about handling the situations where things go wrong.
A request can be duplicated. A connection can disappear. A server can process an operation while the client waits. A retry can reach another application server.
Idempotency gives developers a way to design for those situations instead of hoping they never happen.
The best time to think about retries is before users encounter them.
This article was created with the assistance of AI and reviewed for technical accuracy before publication.
Top comments (0)