27 September 2026
Idempotency in APIs: Designing Operations That Can Be Safely Retried
A comprehensive guide to idempotency in API design, exploring why retries happen, how idempotency keys work, and practical system design considerations for building resilient distributed systems.
In distributed systems, failure is not an anomaly; it is a fundamental property of the environment. Networks drop packets, servers crash unexpectedly, and connections time out. When building APIs, engineers must assume that clients will occasionally lose contact with the server. When this happens, clients usually do what makes the most sense: they retry the request.
However, retrying a request can be dangerous if the operation involves a side effect—like processing a payment or creating an order. To solve this, APIs must be designed to handle duplicate requests safely. This concept is known as idempotency.
This article explores what idempotency means in the context of API design, why it is critical for system reliability, and how to implement it robustly to ensure operations can be safely retried.
What is Idempotency?
In mathematics and computer science, an operation is idempotent if applying it multiple times produces the same result as applying it once.
In the context of HTTP and API design (specifically defined in RFC 9110), an idempotent method is one where the intended effect on the server of multiple identical requests is the same as the effect for a single request.
It is crucial to understand that idempotency refers to the server’s state, not necessarily the HTTP response. For example, if you send an HTTP DELETE request to remove a resource, the first request might succeed and return a 200 OK or 204 No Content. If you send the exact same DELETE request again, the server might return a 404 Not Found because the resource is already gone. Despite the different response codes, the operation is idempotent because the final state of the server (the resource does not exist) remains identical in both cases.
Idempotency vs. “Safe” Methods
It is important to distinguish between operations that are “safe” and those that are “idempotent.”
In HTTP semantics, a method is considered safe if it is essentially read-only and does not alter the state of the server. GET, HEAD, and OPTIONS are safe methods. Because they do not change state, they are inherently idempotent—you can fetch a resource as many times as you want without breaking anything.
Methods like PUT and DELETE are idempotent but not safe. They mutate state, but repeating the exact same mutation multiple times does not result in additional side effects.
POST and PATCH are neither safe nor idempotent by default. Sending the same POST request to create a user account three times could result in three separate user accounts being created.
Idempotency Does Not Mean “No Side Effects”
A common misconception is that an idempotent operation has no side effects. This is incorrect. An idempotent operation absolutely can—and usually does—have side effects.
Creating a database record, triggering an email, or charging a credit card are all side effects. Idempotency guarantees that repeated identical requests produce the same intended effect as processing the request once, even if the underlying implementation involves multiple side effects.
Why Retries Happen in Real Systems
To understand why idempotency is necessary, we must understand why clients retry requests. In a standard client-server interaction, a successful round-trip involves the client sending a request, the server processing it, and the server sending a response back.
Failures can occur at any point in this lifecycle:
- Network Timeouts: The client sends the request, but the network drops it before it reaches the server.
- Connection Failures: The connection drops while the client is transmitting the payload.
- Load Balancers and Proxies: A reverse proxy or load balancer might fail to route the request to a healthy backend server, or the backend might drop the connection, prompting the proxy to retry.
- Temporary Server Failures: The server receives the request but crashes or runs out of memory before it can finish processing.
- The Missing Response: This is the most dangerous scenario. The server successfully receives the request, processes it, creates the database records, and sends the response. However, the network drops the response on its way back to the client.
From the client’s perspective, a timeout due to a dropped request looks exactly the same as a timeout due to a dropped response. The client only knows that it did not receive an answer. Modern HTTP clients and SDKs are often configured to automatically retry idempotent requests (like GET or PUT) when they encounter network timeouts or 5xx server errors.
The Danger of Retrying Non-Idempotent Operations
When an operation creates a side effect, a blind retry can lead to severe data integrity issues or financial loss. Consider these scenarios:
- Processing a Payment: A customer clicks “Pay.” The server processes the charge with the payment gateway, but the client times out waiting for the response. The customer (or their browser) retries the request. Without idempotency, the customer is charged twice.
- Creating an Order: An e-commerce system receives an order submission. It decrements inventory and creates a shipping label. A retry creates a duplicate order, draining inventory and dispatching two packages.
- Sending an External Communication: An API endpoint triggers a push notification or an email. Retries result in the user being spammed with identical messages.
- Submitting a Form: A user submits a support ticket. A network blip causes the client to retry, resulting in multiple identical tickets in the support queue.
In all these cases, the system needs a mechanism to recognize that the second request is a duplicate of the first, and instead of executing the side effects again, it should return the cached outcome of the original request.
Idempotency Keys: Making POST Requests Safe
To make non-idempotent methods (like POST) safe to retry, the industry standard approach is the use of an Idempotency Key. This pattern was popularized by payment processors like Stripe and has become a de facto standard for robust API design.
An explicitly idempotent API operation requires the client to generate a unique identifier for the specific operation they are trying to perform. This identifier is sent to the server, typically in an HTTP header named Idempotency-Key.
How Idempotency Keys Work
- Client Generation: Before sending a request, the client generates a unique key (usually a V4 UUID).
- First Request: The client sends the HTTP request containing the
Idempotency-Keyheader. - Server Processing:
- The server checks its database or caching layer to see if this key has been seen before.
- If it is a new key, the server processes the request normally, executes the side effects, and saves the final response payload and HTTP status code alongside the idempotency key.
- The server returns the response to the client.
- The Retry: If the client fails to receive the response, it retries the exact same request, using the exact same idempotency key.
- Server Handling of the Retry:
- The server checks the key, finds a match, and realizes this operation has already been processed.
- Instead of re-running the business logic, the server simply returns the stored response from the original request.
A Practical Request/Response Example
Let’s look at an example of processing a payment.
Initial Request:
POST /v1/charges HTTP/1.1
Host: api.example.com
Content-Type: application/json
Idempotency-Key: 8b4a2b9e-1f7c-4d3a-9b5e-2a1c8f9e0d4b
{
"amount": 5000,
"currency": "usd",
"customer_id": "cus_12345"
}
The server processes the charge successfully and stores the result. However, the client’s connection times out before receiving it.
The Retry Request:
The client sends the identical request again.
POST /v1/charges HTTP/1.1
Host: api.example.com
Content-Type: application/json
Idempotency-Key: 8b4a2b9e-1f7c-4d3a-9b5e-2a1c8f9e0d4b
{
"amount": 5000,
"currency": "usd",
"customer_id": "cus_12345"
}
The Server Response to the Retry:
The server sees the Idempotency-Key, retrieves the saved result, and returns it immediately without charging the customer again.
HTTP/1.1 200 OK
Content-Type: application/json
Idempotency-Key: 8b4a2b9e-1f7c-4d3a-9b5e-2a1c8f9e0d4b
Original-Request: false
{
"id": "ch_98765",
"amount": 5000,
"currency": "usd",
"status": "succeeded"
}
Implementation Considerations
Implementing idempotency keys requires careful system design. The server must manage state, handle concurrent requests, and validate data integrity.
What the Server Needs to Store
To implement this pattern, the server needs a persistent store (like PostgreSQL, Redis, or DynamoDB) to track idempotency records. A typical idempotency record includes:
- The Idempotency Key: The unique identifier provided by the client.
- The Request Hash or Payload: A cryptographic hash of the request body and relevant headers. This is critical for detecting payload mismatches.
- The State of the Request: Usually an enum like
STARTED,COMPLETED, orFAILED. - The Response Code: The HTTP status code of the final response (e.g.,
201,400). - The Response Payload: The actual body of the response to be returned on subsequent retries.
- An Expiration Timestamp (TTL): When the key can be safely purged.
Handling Retries While the Original Request is Processing
What happens if a client sets a short timeout (e.g., 2 seconds), the server takes 4 seconds to process the request, and the client retries while the first request is still running?
This results in concurrent duplicate requests. If the server is not careful, both threads might bypass the initial idempotency check and execute the operation twice.
To prevent this, the server must use distributed locks or atomic database operations. When a request arrives with an idempotency key:
- The server attempts to atomically insert a record for the key with a state of
STARTED. - If the insert succeeds, this thread has the lock and proceeds to process the business logic.
- If the insert fails (because the key already exists), the server checks the state.
- If the state is
STARTED, it means another thread is currently processing this request. One possible approach is to return a409 Conflictor429 Too Many Requestsresponse indicating that the operation is already being processed, allowing the client to back off and retry later.
Handling Payload Mismatches
A common edge case occurs when a client reuses an idempotency key but changes the request payload. For example, they try to charge $50, the request fails, and they retry with the same key but change the amount to $60.
This is a developer error on the client side. The server must detect this by comparing the hash of the incoming request payload with the hash stored in the idempotency record. If the hashes do not match, the server should reject the request with an HTTP 400 Bad Request, indicating that the key has already been used for a different payload.
Idempotency Key Validity Duration (TTL)
Idempotency keys should not be stored forever. Over time, the storage requirements would grow unbounded. Keys need a Time-To-Live (TTL).
Determining the right TTL is a business and system design decision.
- A 24-hour TTL is standard for many APIs. It provides ample time for clients to retry failed requests during temporary outages.
- Financial systems or highly critical APIs might store keys for 30 days or more to support manual reconciliation and long-running asynchronous retry queues.
Once the TTL expires, the key is purged. If a client sends a request with an expired key, the server treats it as a brand-new request. Therefore, the TTL must be significantly longer than any reasonable client-side retry window.
The Relationship with System Architecture
Idempotency is not an isolated API feature; it is deeply intertwined with how the backend system handles data consistency.
Database Transaction Boundaries
Idempotency logic must respect database transaction boundaries. If your API operation involves inserting an order and charging a user, these actions should ideally be wrapped in a transaction alongside the update to the idempotency record.
If the idempotency record is marked as COMPLETED but the database transaction rolls back due to a constraint violation, the system is now in an inconsistent state. Subsequent retries will return a success response, even though the order was never actually saved.
The idempotency state must be designed so that it cannot report successful completion when the corresponding business operation was not successfully committed; the mechanism for achieving this depends on whether the operation involves local database changes or external side effects.
Unique Constraints
While idempotency keys protect the API layer, database-level unique constraints act as the ultimate safety net.
If an API creates a user account, placing a UNIQUE constraint on the email_address column ensures that even if the idempotency layer fails or a bug is introduced, the database will refuse to create duplicate accounts. Idempotency provides a clean, predictable API experience (returning the original user object instead of a database error), while unique constraints provide hard data integrity.
Downstream API Calls
Idempotency becomes complex in microservice architectures. If Service A receives an idempotent request and must call Service B to fulfill it, Service A must propagate idempotency to Service B.
If Service A fails after calling Service B, a retry from the client will trigger Service A to run again. If Service A does not pass an idempotency key to Service B, Service B might execute its side effects twice. When designing distributed workflows, idempotency keys should be deterministically generated and passed down the call chain to ensure the entire distributed transaction remains safe to retry.
Common Mistakes When Implementing Idempotency
When engineering idempotency layers, certain pitfalls are common:
- Relying Solely on the Client: Servers must strictly enforce idempotency. Relying on the client to “behave nicely” and not send duplicates without an idempotency key defeats the purpose.
- Caching Failures Improperly: Whether a failed response should be retained against an idempotency key depends on the nature of the failure and whether the server can determine whether the operation was actually performed; transient or uncertain failures require particular care because the underlying operation may already have occurred. The idempotency record should be deleted or marked as failed so the client can safely retry and potentially succeed. Only deterministic, business-logic errors (like
400 Bad Requestor422 Unprocessable Entity) should be permanently cached against the key. - Generating Keys on the Server: The idempotency key must be generated by the client before the first network hop. If the server generates the key, a network failure during the initial request means the client never receives the key and cannot safely retry.
- Ignoring Concurrent Requests: Failing to use atomic locks or database constraints when checking key existence leads to race conditions where concurrent retries bypass the idempotency check entirely.
Conclusion
Idempotency is a foundational concept for building robust APIs. In a world where network unreliability is guaranteed, APIs must be designed under the assumption that clients will retry requests.
By leveraging idempotent HTTP methods where appropriate and implementing idempotency keys for state-mutating operations, engineers can abstract away the complexity of network failures. This allows clients to implement aggressive, reliable retry strategies without the fear of causing data corruption, duplicate charges, or inconsistent system states.
Building an idempotency layer requires careful attention to state management, concurrency, and transaction boundaries, but the resulting stability and predictability are essential for any production-grade distributed system.
