Digital Engineering

Idempotency in Distributed Systems — How to Build APIs That Are Safe to Retry

Idempotency in Distributed Systems — How to Build APIs That Are Safe to Retry

Duplicate payments and order processing are critical failures. Learn how to architect idempotent APIs to ensure every request results in exactly one state change, regardless of retries.

Duplicate payments and order processing are critical failures. Learn how to architect idempotent APIs to ensure every request results in exactly one state change, regardless of retries.

08 min read

In the architecture of modern distributed systems, failure is not an anomaly; it is a fundamental expectation. Network timeouts, service crashes, partial deployments, and packet loss are inevitable. When a client sends a request to a server, the "three-way handshake" or the eventual acknowledgment can fail at any stage.

When a client receives a timeout or a 5xx error, the most common corrective action is to retry the request. However, in a distributed system, retrying blindly is dangerous. If the first request actually reached the server and was processed—but the acknowledgment was lost on the return trip—a retry could result in duplicate charges, double-processed orders, or corrupted data.

Idempotency is the property of an operation whereby it can be applied multiple times without changing the result beyond the initial application. Building idempotent APIs is the gold standard for ensuring system reliability and data consistency in the face of partial failures.

1. The Mathematical and Technical Foundation

An operation $f(x)$ is idempotent if:


$$f(f(x)) = f(x)$$

In the context of HTTP and RESTful APIs, this means that making multiple identical requests has the same effect on the server state as making a single request.

HTTP Method Idempotency

Not all HTTP methods are inherently idempotent. Developers must understand these definitions to design compliant APIs:

Method

Idempotent?

Description

GET

Yes

Retrieves data; does not change server state.

PUT

Yes

Replaces a resource at a specific URI; multiple puts to the same URI result in the same state.

DELETE

Yes

Removes a resource; multiple deletions result in the resource being gone.

POST

No

Typically creates a new resource; multiple posts create multiple resources.

PATCH

No

Partially updates a resource; depending on implementation, it may change state cumulatively.

While PUT and DELETE are semantically idempotent, they are only as idempotent as your backend implementation. A DELETE that returns "1 row deleted" is idempotent, but a DELETE that "decrements a counter" is not.

2. The Core Problem: Why Do We Need Idempotency?

Consider the "Lost Acknowledgement" scenario:

  1. Client sends a "Transfer $100" request to the Payment Gateway.

  2. Payment Gateway successfully processes the payment.

  3. Network fails while sending the "Success" response back to the Client.

  4. Client times out and retries the "Transfer $100" request.

  5. Payment Gateway processes the second request.

  6. Result: The customer has been charged $200.

Without an idempotency mechanism, the client is forced to choose between potential data inconsistency and bad user experience.

3. Strategies for Implementing Idempotency

To build a robust system, you must implement a mechanism that allows the server to recognize "seen" requests.

A. Idempotency Keys (The Header Approach)

This is the industry-standard approach used by companies like Stripe and Adyen.

  • Mechanism: The client generates a unique identifier (a UUID) for the transaction and includes it in the HTTP header (e.g., Idempotency-Key: <UUID>).

  • Server Lifecycle:

    1. The server receives the request and checks if the key exists in a cache (e.g., Redis).

    2. If the key exists and a response is stored, the server returns the cached response without re-executing the logic.

    3. If the key does not exist, the server executes the business logic, stores the result, and returns the response.

B. Database Constraints (Unique Indexes)

For create operations, you can rely on the database layer to enforce idempotency. If you have an order_id generated by the client, you can define it as a UNIQUE constraint in your database schema.

  • Pros: Guaranteed integrity at the storage layer.

  • Cons: Handling the error requires graceful logic to return a "200 OK" or "204 No Content" instead of a "500 Internal Server Error" when the collision occurs.

C. State Machine Transitions

If your resource has a status (e.g., PENDING, COMPLETED, CANCELLED), you can implement idempotency by validating state transitions.

Example: An order can only transition from PENDING to COMPLETED. If a retry arrives for an order that is already COMPLETED, the server simply returns success without running the payment logic again.

4. Architectural Implementation Patterns

Implementing idempotency requires a shift in how you handle requests.

The "Check-Act-Store" Pattern

This is the most common pattern for handling high-throughput idempotent requests.

  1. Check: Does the Idempotency Key exist in the store (Redis/Database)?

  2. Act: If not, acquire a distributed lock on the key, perform the business transaction (e.g., talk to the bank), and store the result.

  3. Store: Save the final HTTP response (status code and body) associated with the key with a reasonable Time-To-Live (TTL).

Managing Distributed Locks

When building high-concurrency systems, two identical requests might arrive at different server nodes simultaneously. If you don't use a lock, both nodes might check the cache, see that the key is missing, and both proceed to process the transaction.

  • Use Redis Redlock or a database-level advisory lock to ensure that even if two requests arrive at the same millisecond, only one executes the business logic.

5. Implementation Checklist for Engineers

Building safe APIs is not just about the code; it is about the lifecycle of the data.

  • 1. Client Generation: Clients must be responsible for generating the idempotency keys. Use version 4 UUIDs to minimize collision risk.

  • 2. Expiration Policies: Do not store idempotency keys forever. A TTL of 24–48 hours is standard. After that, assume the client has either succeeded or abandoned the request.

  • 3. Response Replay: Ensure the stored response is exactly what was returned the first time. If you returned a 201 Created with a specific resource ID, the retried request must return the exact same data.

  • 4. Error Handling: If a client tries to reuse an idempotency key for a different request (a mismatch), the server must return a 400 Bad Request or 422 Unprocessable Entity. Do not reuse keys for different operations.

  • 5. Transactional Integrity: The storage of the business result and the storage of the idempotency key must happen within the same atomic transaction. If you save the payment but fail to save the idempotency key, you have lost your record of completion.

6. Challenges and Trade-offs
The Performance Overhead

Adding a check to a Redis cache for every request adds latency (typically 1–5ms). While negligible for most systems, it can become a bottleneck in massive-scale financial systems.

Partial Success (The "Zombie" State)

What happens if your business logic involves multiple microservices?

  • Service A calls Service B (Payment).

  • Service A calls Service C (Inventory).

If Service B succeeds but Service C fails, the retry will trigger the idempotency check. Service B will return the "success" result from its cache, but Service C will attempt to update inventory again. To solve this, you need distributed transactions (e.g., Saga Pattern) or event-driven eventual consistency.

7. Comparison of Idempotency Approaches

Approach

Implementation Complexity

Data Integrity

Best For

Idempotency Headers

Medium

High

Public APIs, Payment Gateways

Database Constraints

Low

Very High

Simple resource creation

State Machine

High

Very High

Order management, Workflows

Client-Side Deduplication

Medium

Moderate

Mobile/Offline-first apps

8. Real-World Case Study: Financial Services

Most major payment processors require an Idempotency-Key. If you look at the Stripe API, they explicitly warn:

"If you use the same key for two different requests, we will return the response from the first request."

This forces developers to treat the key as a "unit of work" rather than a "request identifier." In a distributed payment system, the flow is:

  1. Request Initiation: The server receives the request.

  2. Locking: Use a distributed lock based on the Idempotency-Key (e.g., lock:order_123).

  3. Persistence: Within a single database transaction, write the order to the orders table and write the response to the idempotency_responses table.

  4. Release: Release the lock and return the response.

If the server crashes during step 3, the database transaction rolls back, and the client receives no response. The client retries, the lock is acquired, and the transaction starts from scratch. If the server crashes after step 3, the response is in the database; upon retry, the server detects the existing record and serves it back without re-processing.

9. Handling "In-Flight" Requests

One of the most complex edge cases is the "In-Flight" request. This occurs when a request is still processing, but the client sends the same request again.

  • Option A: Fail Fast. Return a 409 Conflict (or 429 Too Many Requests) indicating that a request with this key is already being processed. This is highly recommended as it prevents unnecessary server load.

  • Option B: Wait (Polling/Blocking). Have the server hold the request open until the first process finishes. This is technically difficult to manage at scale due to connection limits and timeout overheads.

For most distributed systems, Option A is the preferred path. It forces the client to handle the "wait" logic, which is cleaner and more scalable.

10. Summary and Future Trends

As we move toward more granular microservices, the necessity of idempotency grows. Systems are becoming more asynchronous, utilizing event buses like Kafka or RabbitMQ.

In an event-driven architecture, idempotency is handled by the consumer. When a service consumes a message from a queue, it must check if the message_id has already been processed before executing its task. This "exactly-once delivery" requirement is effectively "at-least-once delivery plus idempotency."

Final Considerations
  1. Observability: Always log when an idempotency key collision occurs. If you see a high volume of key collisions, it indicates a bug in your client-side retry logic.

  2. Security: Idempotency keys should be treated with the same sensitivity as authentication tokens. Ensure they are not exposed in logs or insecure storage.

  3. Simplicity: Do not over-engineer. If your API is strictly read-only, you do not need idempotency. Apply it only to state-changing operations where the cost of a duplicate is high.

Building for idempotency is a sign of a mature engineering team. It demonstrates an understanding that infrastructure is fallible and that the responsibility for data integrity must be shared between the client and the server. By implementing these strategies, you move your system away from "hope-based engineering" and toward a robust, self-healing architecture that handles the reality of the network with confidence.

Implementation Workflow Visualization
  • Client sends Request + Idempotency-Key.

  • API Gateway routes request to Service.

  • Service checks Global Cache.

  • Flow Split: * If Found: Return Result.

    • If Not Found: Execute logic -> Save Result -> Return.

By adhering to these patterns, you ensure that even under heavy network turbulence, your system maintains a "single version of the truth." Your APIs become not just functional, but reliable, predictable, and—most importantly—resilient.

Recommended Best Practices Table

Category

Recommendation

Communication

Always document expected behavior for retries in API specs (OpenAPI/Swagger).

Design

Ensure idempotency logic is in the application layer, not the infrastructure.

Testing

Write integration tests that simulate "timeout then retry" scenarios.

Monitoring

Set up alerts for unexpected idempotency key collisions (indicates logic errors).

Security

Rate limit requests by Idempotency-Key to prevent Denial-of-Service attacks.

The transition from a naive API to an idempotent one is the most significant step an engineer can take to improve the "Mean Time To Recovery" (MTTR) of their services. By ensuring that every request is safe to repeat, you remove the fear of network volatility and empower your users to interact with your system with the assurance that their commands—and only their commands—will be executed exactly as intended.

In the architecture of modern distributed systems, failure is not an anomaly; it is a fundamental expectation. Network timeouts, service crashes, partial deployments, and packet loss are inevitable. When a client sends a request to a server, the "three-way handshake" or the eventual acknowledgment can fail at any stage.

When a client receives a timeout or a 5xx error, the most common corrective action is to retry the request. However, in a distributed system, retrying blindly is dangerous. If the first request actually reached the server and was processed—but the acknowledgment was lost on the return trip—a retry could result in duplicate charges, double-processed orders, or corrupted data.

Idempotency is the property of an operation whereby it can be applied multiple times without changing the result beyond the initial application. Building idempotent APIs is the gold standard for ensuring system reliability and data consistency in the face of partial failures.

1. The Mathematical and Technical Foundation

An operation $f(x)$ is idempotent if:


$$f(f(x)) = f(x)$$

In the context of HTTP and RESTful APIs, this means that making multiple identical requests has the same effect on the server state as making a single request.

HTTP Method Idempotency

Not all HTTP methods are inherently idempotent. Developers must understand these definitions to design compliant APIs:

Method

Idempotent?

Description

GET

Yes

Retrieves data; does not change server state.

PUT

Yes

Replaces a resource at a specific URI; multiple puts to the same URI result in the same state.

DELETE

Yes

Removes a resource; multiple deletions result in the resource being gone.

POST

No

Typically creates a new resource; multiple posts create multiple resources.

PATCH

No

Partially updates a resource; depending on implementation, it may change state cumulatively.

While PUT and DELETE are semantically idempotent, they are only as idempotent as your backend implementation. A DELETE that returns "1 row deleted" is idempotent, but a DELETE that "decrements a counter" is not.

2. The Core Problem: Why Do We Need Idempotency?

Consider the "Lost Acknowledgement" scenario:

  1. Client sends a "Transfer $100" request to the Payment Gateway.

  2. Payment Gateway successfully processes the payment.

  3. Network fails while sending the "Success" response back to the Client.

  4. Client times out and retries the "Transfer $100" request.

  5. Payment Gateway processes the second request.

  6. Result: The customer has been charged $200.

Without an idempotency mechanism, the client is forced to choose between potential data inconsistency and bad user experience.

3. Strategies for Implementing Idempotency

To build a robust system, you must implement a mechanism that allows the server to recognize "seen" requests.

A. Idempotency Keys (The Header Approach)

This is the industry-standard approach used by companies like Stripe and Adyen.

  • Mechanism: The client generates a unique identifier (a UUID) for the transaction and includes it in the HTTP header (e.g., Idempotency-Key: <UUID>).

  • Server Lifecycle:

    1. The server receives the request and checks if the key exists in a cache (e.g., Redis).

    2. If the key exists and a response is stored, the server returns the cached response without re-executing the logic.

    3. If the key does not exist, the server executes the business logic, stores the result, and returns the response.

B. Database Constraints (Unique Indexes)

For create operations, you can rely on the database layer to enforce idempotency. If you have an order_id generated by the client, you can define it as a UNIQUE constraint in your database schema.

  • Pros: Guaranteed integrity at the storage layer.

  • Cons: Handling the error requires graceful logic to return a "200 OK" or "204 No Content" instead of a "500 Internal Server Error" when the collision occurs.

C. State Machine Transitions

If your resource has a status (e.g., PENDING, COMPLETED, CANCELLED), you can implement idempotency by validating state transitions.

Example: An order can only transition from PENDING to COMPLETED. If a retry arrives for an order that is already COMPLETED, the server simply returns success without running the payment logic again.

4. Architectural Implementation Patterns

Implementing idempotency requires a shift in how you handle requests.

The "Check-Act-Store" Pattern

This is the most common pattern for handling high-throughput idempotent requests.

  1. Check: Does the Idempotency Key exist in the store (Redis/Database)?

  2. Act: If not, acquire a distributed lock on the key, perform the business transaction (e.g., talk to the bank), and store the result.

  3. Store: Save the final HTTP response (status code and body) associated with the key with a reasonable Time-To-Live (TTL).

Managing Distributed Locks

When building high-concurrency systems, two identical requests might arrive at different server nodes simultaneously. If you don't use a lock, both nodes might check the cache, see that the key is missing, and both proceed to process the transaction.

  • Use Redis Redlock or a database-level advisory lock to ensure that even if two requests arrive at the same millisecond, only one executes the business logic.

5. Implementation Checklist for Engineers

Building safe APIs is not just about the code; it is about the lifecycle of the data.

  • 1. Client Generation: Clients must be responsible for generating the idempotency keys. Use version 4 UUIDs to minimize collision risk.

  • 2. Expiration Policies: Do not store idempotency keys forever. A TTL of 24–48 hours is standard. After that, assume the client has either succeeded or abandoned the request.

  • 3. Response Replay: Ensure the stored response is exactly what was returned the first time. If you returned a 201 Created with a specific resource ID, the retried request must return the exact same data.

  • 4. Error Handling: If a client tries to reuse an idempotency key for a different request (a mismatch), the server must return a 400 Bad Request or 422 Unprocessable Entity. Do not reuse keys for different operations.

  • 5. Transactional Integrity: The storage of the business result and the storage of the idempotency key must happen within the same atomic transaction. If you save the payment but fail to save the idempotency key, you have lost your record of completion.

6. Challenges and Trade-offs
The Performance Overhead

Adding a check to a Redis cache for every request adds latency (typically 1–5ms). While negligible for most systems, it can become a bottleneck in massive-scale financial systems.

Partial Success (The "Zombie" State)

What happens if your business logic involves multiple microservices?

  • Service A calls Service B (Payment).

  • Service A calls Service C (Inventory).

If Service B succeeds but Service C fails, the retry will trigger the idempotency check. Service B will return the "success" result from its cache, but Service C will attempt to update inventory again. To solve this, you need distributed transactions (e.g., Saga Pattern) or event-driven eventual consistency.

7. Comparison of Idempotency Approaches

Approach

Implementation Complexity

Data Integrity

Best For

Idempotency Headers

Medium

High

Public APIs, Payment Gateways

Database Constraints

Low

Very High

Simple resource creation

State Machine

High

Very High

Order management, Workflows

Client-Side Deduplication

Medium

Moderate

Mobile/Offline-first apps

8. Real-World Case Study: Financial Services

Most major payment processors require an Idempotency-Key. If you look at the Stripe API, they explicitly warn:

"If you use the same key for two different requests, we will return the response from the first request."

This forces developers to treat the key as a "unit of work" rather than a "request identifier." In a distributed payment system, the flow is:

  1. Request Initiation: The server receives the request.

  2. Locking: Use a distributed lock based on the Idempotency-Key (e.g., lock:order_123).

  3. Persistence: Within a single database transaction, write the order to the orders table and write the response to the idempotency_responses table.

  4. Release: Release the lock and return the response.

If the server crashes during step 3, the database transaction rolls back, and the client receives no response. The client retries, the lock is acquired, and the transaction starts from scratch. If the server crashes after step 3, the response is in the database; upon retry, the server detects the existing record and serves it back without re-processing.

9. Handling "In-Flight" Requests

One of the most complex edge cases is the "In-Flight" request. This occurs when a request is still processing, but the client sends the same request again.

  • Option A: Fail Fast. Return a 409 Conflict (or 429 Too Many Requests) indicating that a request with this key is already being processed. This is highly recommended as it prevents unnecessary server load.

  • Option B: Wait (Polling/Blocking). Have the server hold the request open until the first process finishes. This is technically difficult to manage at scale due to connection limits and timeout overheads.

For most distributed systems, Option A is the preferred path. It forces the client to handle the "wait" logic, which is cleaner and more scalable.

10. Summary and Future Trends

As we move toward more granular microservices, the necessity of idempotency grows. Systems are becoming more asynchronous, utilizing event buses like Kafka or RabbitMQ.

In an event-driven architecture, idempotency is handled by the consumer. When a service consumes a message from a queue, it must check if the message_id has already been processed before executing its task. This "exactly-once delivery" requirement is effectively "at-least-once delivery plus idempotency."

Final Considerations
  1. Observability: Always log when an idempotency key collision occurs. If you see a high volume of key collisions, it indicates a bug in your client-side retry logic.

  2. Security: Idempotency keys should be treated with the same sensitivity as authentication tokens. Ensure they are not exposed in logs or insecure storage.

  3. Simplicity: Do not over-engineer. If your API is strictly read-only, you do not need idempotency. Apply it only to state-changing operations where the cost of a duplicate is high.

Building for idempotency is a sign of a mature engineering team. It demonstrates an understanding that infrastructure is fallible and that the responsibility for data integrity must be shared between the client and the server. By implementing these strategies, you move your system away from "hope-based engineering" and toward a robust, self-healing architecture that handles the reality of the network with confidence.

Implementation Workflow Visualization
  • Client sends Request + Idempotency-Key.

  • API Gateway routes request to Service.

  • Service checks Global Cache.

  • Flow Split: * If Found: Return Result.

    • If Not Found: Execute logic -> Save Result -> Return.

By adhering to these patterns, you ensure that even under heavy network turbulence, your system maintains a "single version of the truth." Your APIs become not just functional, but reliable, predictable, and—most importantly—resilient.

Recommended Best Practices Table

Category

Recommendation

Communication

Always document expected behavior for retries in API specs (OpenAPI/Swagger).

Design

Ensure idempotency logic is in the application layer, not the infrastructure.

Testing

Write integration tests that simulate "timeout then retry" scenarios.

Monitoring

Set up alerts for unexpected idempotency key collisions (indicates logic errors).

Security

Rate limit requests by Idempotency-Key to prevent Denial-of-Service attacks.

The transition from a naive API to an idempotent one is the most significant step an engineer can take to improve the "Mean Time To Recovery" (MTTR) of their services. By ensuring that every request is safe to repeat, you remove the fear of network volatility and empower your users to interact with your system with the assurance that their commands—and only their commands—will be executed exactly as intended.

FAQs

get in touch

Ready to Grow From Day One?

Strategy, execution, and digital experiences designed to move together. Fill out the form below and our team will contact you shortly.

get in touch

Ready to Grow From Day One?

Strategy, execution, and digital experiences designed to move together. Fill out the form below and our team will contact you shortly.

get in touch

Ready to Grow From Day One?

Strategy, execution, and digital experiences designed to move together. Fill out the form below and our team will contact you shortly.

© 2026 projectsupply AI, Data and Digital Engineering 

Company. Pune, India. All rights reserved.

Part of Tangle

© 2026 projectsupply AI, Data and Digital Engineering 

Company. Pune, India. All rights reserved.

Part of Tangle

© 2026 projectsupply AI, Data and Digital Engineering 

Company. Pune, India. All rights reserved.

Part of Tangle