Use an Idempotency-Key for HTTP Requests
When you build an API, one important question is: **What happens if the same request is sent twice?** Sometimes, nothing bad happens. But sometimes, sending the same request twice can create duplicate data or perform the same operation twice.
When you build an API, one important question is:
What happens if the same request is sent twice?
Sometimes, nothing bad happens. But sometimes, sending the same request twice can create duplicate data or perform the same operation twice.
This is where idempotency becomes important.
What is an idempotent request?
A request is idempotent when performing it multiple times has the same intended effect on the server as performing it once.
For example:
PUT /users/123
with:
{
"name": "John"
}
If we send this request once, the user's name becomes John.
If we send exactly the same request ten times, the final state is still:
name = John
The operation is idempotent.
HTTP methods
Some HTTP methods are defined as idempotent:
- GET — reads data and does not modify the resource.
- PUT — replaces the resource with the provided representation. Repeating the same request produces the same final state.
- DELETE — removes the resource. Repeating the operation does not remove it again; the resource remains absent.
However, other methods can produce a different result every time.
POST
Consider:
POST /payments
If the client sends this request twice, the server might create two payments:
Payment #123
Payment #124
This can be a serious problem.
Imagine that the client sends a payment request, but the network connection fails before the client receives the response.
The client doesn't know whether the server processed the payment.
So it retries:
Client → POST /payments
← payment created
X response lost
Client → POST /payments
← another payment created
Now we have a duplicate payment.
This is one of the problems that idempotency can solve.
Note:
PATCHis not generally guaranteed to be idempotent, andUPDATEis not an HTTP method.
Why do we need idempotency?
In real systems, requests can be retried for many reasons:
- Network failures
- Timeouts
- Client retries
- Load balancers
- Message queues
- Distributed systems
- Temporary server errors
The client may not know whether the first request was successfully processed.
For operations such as payments, orders, subscriptions, or resource creation, simply retrying the request can be dangerous.
We need a way for the server to understand:
"I've already processed this operation."
One common solution is an Idempotency-Key.
Using an Idempotency-Key
The client generates a unique identifier for the operation and sends it in a request header:
POST /payments
Idempotency-Key: 7f8b9c2a-...
The server stores the key when it processes the request.
If the client sends the same request again with the same key, the server knows that it has already processed that operation.
Instead of creating another payment, it returns the original result.
Conceptually:
First request:
Idempotency-Key: abc-123
↓
Process request
↓
Save response
↓
Return response
Second request:
Idempotency-Key: abc-123
↓
Already processed?
↓
YES
↓
Return original response
This convention is widely used in APIs, including Stripe's API.
Three important details
There are three details that I think are especially important when implementing this strategy.
1. The key is not globally unique
An idempotency key generated by a client should not necessarily be treated as globally unique.
For example:
user_id + route + idempotency_key
could identify the operation.
Why?
Because two different clients could theoretically generate the same key:
User A → abc-123
User B → abc-123
These are two different operations.
Therefore, the idempotency key should be scoped to the appropriate client or user context.
2. Store the response, not only the key
This is an important detail.
A naive implementation might store only:
idempotency_key = abc-123
and then return a generic response when the key appears again.
But the better approach is to store the result of the original request:
key
status_code
response_body
response_headers
For example:
abc-123
201
{
"id": "payment_123",
"status": "created"
}
When the duplicate request arrives, we return the same result.
From the client's perspective, it looks like the original request succeeded normally.
The client doesn't need to know whether this is the first request or a retry.
3. Validate the request body
There is another important problem.
What happens if the client reuses the same key with a different body?
For example:
Idempotency-Key: abc-123
First request:
{
"amount": 100
}
Then:
Idempotency-Key: abc-123
Second request:
{
"amount": 500
}
This should not be treated as a normal duplicate request.
The idempotency key identifies an operation, so the same key should correspond to the same request parameters.
A common solution is to calculate a hash of the request body:
idempotency_key
+
request_body_hash
Then we can distinguish between:
abc-123 + hash(body A)
and:
abc-123 + hash(body B)
If the key is the same but the body is different, the server can return an error instead of silently returning the previous response.
This also protects against accidental reuse of an idempotency key.
A simple database model
A possible table could look like this:
idempotency_keys
------------------------------------------------
user_id
route
idempotency_key
request_hash
status_code
response_body
created_at
------------------------------------------------
Then the flow becomes:
1. Receive request
↓
2. Read Idempotency-Key
↓
3. Build the idempotency scope
↓
4. Check if the key already exists
↓
┌───────────────┴───────────────┐
↓ ↓
Doesn't exist Exists
↓ ↓
Process request Compare request hash
↓ ↓
Save result ┌─────┴─────┐
↓ ↓ ↓
Return response Same body Different body
↓ ↓
Return saved Return error
response
The important part is that the idempotency record and the operation need to be handled atomically.
Otherwise, two identical requests arriving at almost the same time could both see that the key doesn't exist and both execute the operation.
The main idea
Idempotency is not about preventing clients from sending the same request twice.
It is about making repeated requests safe.
The client can retry:
POST → timeout
POST → timeout
POST → success
while the server ensures that the operation is only performed once.
For me, the most important idea is this:
Retries are normal in distributed systems. Your API should be designed to handle them safely.
And an Idempotency-Key is one of the simplest patterns we can use to make non-idempotent operations, such as POST, safer.
Relational links: stripe document