Nati's Blog
Building Reliable REST APIs with Idempotency
Introduction
Have you ever double-clicked a "Submit" button and wondered if your order might be placed twice? Or had a payment timeout but weren't sure if the transaction went through? These common user experiences highlight a critical aspect of REST API design: idempotency.
In simple terms, an idempotent REST API ensures that performing the same HTTP request multiple times produces the same result as doing it once. This property is essential for building reliable systems, especially in distributed environments where network failures and retries are common.
Why Idempotency Matters in REST APIs
Consider these real-world scenarios where idempotency in REST APIs can prevent costly errors:
Scenario 1: Payment Processing
A customer submits a payment through your REST API, but their connection drops before receiving confirmation. Without idempotency, retrying the payment request could charge them twice.
Scenario 2: Order Placement
A user repeatedly clicks the "Place Order" button due to a slow response. Each click triggers a separate REST API call. Without idempotency, they might receive duplicate shipments of the same items.
Scenario 3: User Registration
A person submits a registration form, but the confirmation page doesn't load. The client application retries the REST API call, potentially creating duplicate accounts.

Idempotency in REST API HTTP Methods
REST APIs are built on HTTP methods, each with inherent idempotency characteristics:
Idempotent HTTP Methods
Method | Description | Example | Idempotent? |
---|---|---|---|
GET | Retrieves resources without modification | GET /users/123 | ✅ Yes |
PUT | Creates or replaces a resource | PUT /users/123 | ✅ Yes |
DELETE | Removes a resource | DELETE /users/123 | ✅ Yes |
HEAD | Like GET but returns only headers | HEAD /users/123 | ✅ Yes |
OPTIONS | Returns supported HTTP methods | OPTIONS /users/123 | ✅ Yes |
Non-Idempotent HTTP Methods
Method | Description | Example | Idempotent? |
---|---|---|---|
POST | Creates new resources or triggers actions | POST /orders | ❌ No (by default) |
PATCH | Partially updates a resource | PATCH /users/123 | ❌ No (by default) |
While POST and PATCH are typically non-idempotent in REST APIs, we can implement mechanisms to make these operations idempotent, which we'll explore further in this guide.

Understanding Idempotency vs. Retries in REST APIs
It's important to distinguish between idempotency and retries in the context of REST APIs:
- Idempotency is a property of a REST API endpoint that ensures repeating the same request has the same effect as performing it once.
- Retries are client-side mechanisms to handle transient errors (such as network disruptions) by resending the HTTP request.
Retries require idempotent REST API endpoints to work safely. Without idempotency, retry mechanisms can cause data inconsistencies and duplicate records.

Challenges of Implementing Idempotent REST APIs
Building truly idempotent REST APIs comes with several challenges:
- Unique Identifier Management: Generating and tracking request IDs across distributed systems.
- Concurrent Requests: Handling multiple simultaneous HTTP requests with the same ID.
- Partial Failures: Managing operations that fail midway through execution.
- State Management: Tracking processed REST API requests efficiently without degrading performance.
- Cache Expiry: Determining appropriate timeframes for considering HTTP requests as duplicates.
Practical Implementation Strategies for REST APIs
Let's explore four effective strategies to implement idempotency in your REST APIs, with code examples and practical considerations.
Strategy 1: Database Unique Constraints
This approach uses database-level constraints to prevent duplicate operations, making it one of the simplest and most reliable methods for REST APIs.

How It Works:
- Add a unique constraint column (like
transaction_id
) to your database table - Require clients to include this ID in the REST API request (typically as a header)
- Check for constraint violations when processing requests
Code Example:
-- Create a table with a unique transaction_id constraint CREATE TABLE orders ( id SERIAL PRIMARY KEY, user_id INT NOT NULL, product_id INT NOT NULL, quantity INT NOT NULL, transaction_id UUID UNIQUE NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );
# Python Flask REST API example with PostgreSQL import uuid from flask import Flask, request, jsonify from psycopg2 import IntegrityError app = Flask(__name__) @app.route('/api/orders', methods=['POST']) def create_order(): # Get transaction ID from header or generate one transaction_id = request.headers.get('Idempotency-Key') if not transaction_id: return jsonify({"error": "Idempotency-Key header is required"}), 400 # Extract order details from request user_id = request.json.get('user_id') product_id = request.json.get('product_id') quantity = request.json.get('quantity') try: # Try to insert with the transaction_id cursor.execute(""" INSERT INTO orders (user_id, product_id, quantity, transaction_id) VALUES (%s, %s, %s, %s) RETURNING id """, (user_id, product_id, quantity, transaction_id)) order_id = cursor.fetchone()[0] conn.commit() return jsonify({"order_id": order_id, "status": "created"}), 201 except IntegrityError: # This is a duplicate request, find the original order conn.rollback() cursor.execute(""" SELECT id FROM orders WHERE transaction_id = %s """, (transaction_id,)) order_id = cursor.fetchone()[0] return jsonify({"order_id": order_id, "status": "already_exists"}), 200
Advantages for REST APIs:
- Simple to implement and understand
- Leverages built-in database capabilities
- Provides strong consistency guarantees
Limitations:
- Can create database performance bottlenecks at high scale
- Complex error handling required
- Challenging in distributed database environments
Strategy 2: In-Memory Tracking
This lightweight approach uses an in-memory data structure to track processed REST API requests.

How It Works:
- Maintain a hash map or set of processed request IDs in memory
- Check this map before processing each new REST API request
- Add the request ID to the map after successful processing
Code Example:
// Node.js Express REST API middleware for idempotency const processedRequests = new Map(); function idempotencyMiddleware(req, res, next) { const requestId = req.headers['idempotency-key']; if (!requestId) { return res.status(400).json({ error: 'Idempotency-Key header is required' }); } // Check if we've already processed this request if (processedRequests.has(requestId)) { const cachedResponse = processedRequests.get(requestId); return res.status(cachedResponse.status).json(cachedResponse.body); } // Store the original response methods const originalSend = res.send; const originalJson = res.json; const originalStatus = res.status; let responseBody; let responseStatus = 200; // Override response methods to capture the response res.send = function(body) { responseBody = body; return originalSend.call(this, body); }; res.json = function(body) { responseBody = body; return originalJson.call(this, body); }; res.status = function(code) { responseStatus = code; return originalStatus.call(this, code); }; // Add a listener for when the response finishes res.on('finish', () => { // Only cache successful responses if (responseStatus >= 200 && responseStatus < 300) { processedRequests.set(requestId, { status: responseStatus, body: responseBody }); // Optional: Set a TTL to prevent memory leaks setTimeout(() => { processedRequests.delete(requestId); }, 3600000); // 1 hour expiry } }); next(); } // Usage in Express REST API const express = require('express'); const app = express(); app.use(express.json()); // Apply the middleware to POST endpoints that need idempotency app.post('/api/payments', idempotencyMiddleware, (req, res) => { // Process payment logic res.status(201).json({ id: 'payment123', status: 'success' }); });
Advantages for REST APIs:
- Fast performance (in-memory operations)
- Simple implementation with no external dependencies
- Low latency for request processing
Limitations:
- Data is lost on service restart
- Not suitable for distributed REST API servers (multiple instances)
- Memory constraints limit scalability
- Requires memory management and TTL implementation
Strategy 3: Distributed Cache with Redis
Using Redis as a distributed cache provides a scalable solution for tracking idempotency across multiple REST API service instances.

How It Works:
- Use Redis to store REST API request IDs with automatic TTL
- Check Redis before processing each request
- Use atomic operations (SETNX) to prevent race conditions
Code Example:
# Python Flask REST API example with Redis import redis import json from flask import Flask, request, jsonify app = Flask(__name__) redis_client = redis.Redis(host='localhost', port=6379, db=0) @app.route('/api/transfers', methods=['POST']) def transfer_funds(): # Get idempotency key from header idempotency_key = request.headers.get('Idempotency-Key') if not idempotency_key: return jsonify({"error": "Idempotency-Key header is required"}), 400 # Check if we've seen this request before cached_result = redis_client.get(f"idempotent:{idempotency_key}") if cached_result: return json.loads(cached_result), 200 # Try to acquire a lock for this request lock_acquired = redis_client.setnx(f"idempotent:lock:{idempotency_key}", "1") if not lock_acquired: return jsonify({"error": "Concurrent request with same idempotency key"}), 409 # Set lock expiration redis_client.expire(f"idempotent:lock:{idempotency_key}", 30) # 30 seconds try: # Process the transfer from_account = request.json.get('from') to_account = request.json.get('to') amount = request.json.get('amount') # Your business logic here transfer_id = process_transfer(from_account, to_account, amount) # Create response result = { "transfer_id": transfer_id, "status": "completed", "from": from_account, "to": to_account, "amount": amount } # Store the result with TTL (24 hours = 86400 seconds) redis_client.setex( f"idempotent:{idempotency_key}", 86400, json.dumps(result) ) return jsonify(result), 201 finally: # Release the lock redis_client.delete(f"idempotent:lock:{idempotency_key}") def process_transfer(from_account, to_account, amount): # Actual transfer logic here return "transfer_" + str(hash(f"{from_account}:{to_account}:{amount}"))[:8] if __name__ == '__main__': app.run(debug=True)
Advantages for REST APIs:
- Scales across multiple service instances
- Built-in TTL support for automatic cleanup
- Atomic operations prevent race conditions
- High performance with minimal latency
Limitations:
- Requires Redis infrastructure
- Network latency between service and Redis
- TTL management trade-offs
- Additional operational complexity
Strategy 4: Message Duplicate Detection
For event-driven architectures that use REST APIs as entry points, message brokers like Azure Service Bus provide built-in duplicate detection for the subsequent processing.

How It Works:
- The REST API receives a request and publishes a message to a queue
- Configure duplicate detection on your message broker
- Assign unique message IDs to each message
- The broker automatically detects and discards duplicates
Code Example with Azure Service Bus:
// C# ASP.NET Core REST API with Azure Service Bus using Microsoft.AspNetCore.Mvc; using Microsoft.Azure.ServiceBus; using System; using System.Text; using System.Threading.Tasks; using Newtonsoft.Json; [ApiController] [Route("api/[controller]")] public class OrdersController : ControllerBase { private readonly QueueClient _queueClient; public OrdersController(string connectionString) { // Azure Service Bus queue with duplicate detection enabled _queueClient = new QueueClient(connectionString, "orders-queue"); } [HttpPost] public async Task<IActionResult> CreateOrder([FromBody] OrderRequest orderRequest, [FromHeader(Name = "Idempotency-Key")] string idempotencyKey) { if (string.IsNullOrEmpty(idempotencyKey)) { return BadRequest("Idempotency-Key header is required"); } var order = new Order { OrderId = Guid.NewGuid().ToString(), CustomerId = orderRequest.CustomerId, Amount = orderRequest.Amount, Items = orderRequest.Items, Timestamp = DateTime.UtcNow }; // Create message with the unique idempotency key as MessageId var message = new Message(Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(order))) { MessageId = idempotencyKey, ContentType = "application/json" }; // Send the message to the queue // Azure Service Bus will automatically detect and discard duplicates await _queueClient.SendAsync(message); return CreatedAtAction(nameof(GetOrder), new { id = order.OrderId }, order); } [HttpGet("{id}")] public IActionResult GetOrder(string id) { // Implementation to retrieve order details return Ok(new { OrderId = id, Status = "Processing" }); } } public class OrderRequest { public string CustomerId { get; set; } public decimal Amount { get; set; } public OrderItem[] Items { get; set; } } public class Order { public string OrderId { get; set; } public string CustomerId { get; set; } public decimal Amount { get; set; } public DateTime Timestamp { get; set; } public OrderItem[] Items { get; set; } } public class OrderItem { public string ProductId { get; set; } public int Quantity { get; set; } public decimal UnitPrice { get; set; } }
Azure Service Bus setup example:

Advantages for REST APIs:
- Built-in broker functionality handles the hard parts
- Works well in distributed systems
- Minimal code changes required
- Handles high throughput scenarios efficiently
Limitations:
- Limited to the time window configured in the broker
- Vendor-specific implementations
- Additional cost for premium messaging features
- Only handles message-driven workflows downstream from the REST API
Best Practices for Designing Idempotent REST APIs

Follow these guidelines to design robust idempotent REST APIs:
1. Standardize Idempotency Headers
Use consistent header naming across your REST API endpoints:
POST /api/payments HTTP/1.1 Host: api.example.com Content-Type: application/json Idempotency-Key: 123e4567-e89b-12d3-a456-426614174000
Many major REST API providers use standardized headers:
- Stripe:
Idempotency-Key
- PayPal:
PayPal-Request-Id
- AWS:
X-Amz-Client-Token
Choose one convention and use it consistently.
2. Generate Client-Side Idempotency Keys
// Client-side generation of idempotency keys for REST API calls function generateIdempotencyKey(endpoint, payload) { // Combine endpoint with payload hash and user identifier const data = endpoint + JSON.stringify(payload) + userId; // Create a hash (simplified example) return crypto.createHash('sha256').update(data).digest('hex'); } // Usage in fetch call to REST API async function createPayment(paymentDetails) { const idempotencyKey = generateIdempotencyKey('/api/payments', paymentDetails); const response = await fetch('https://api.example.com/api/payments', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Idempotency-Key': idempotencyKey }, body: JSON.stringify(paymentDetails) }); return response.json(); }
3. Document Idempotency Behavior
Clearly indicate which REST API endpoints support idempotency and how to use them:
# OpenAPI (Swagger) specification example paths: /api/payments: post: summary: Create a new payment description: > This endpoint is idempotent when used with the Idempotency-Key header. Repeated requests with the same key will return the same result without creating duplicate payments. parameters: - name: Idempotency-Key in: header required: true schema: type: string format: uuid description: Unique identifier for this request (UUID v4 recommended)
4. Set Appropriate TTL for Idempotency Records
Balance resource usage with operational requirements:
# Example TTL settings for different REST API endpoints IDEMPOTENCY_TTL = { '/api/payments': 24 * 60 * 60, # 24 hours for payments '/api/preferences': 15 * 60, # 15 minutes for preferences updates '/api/events': 5 * 60, # 5 minutes for analytics events } def store_idempotency_record(key, result, endpoint): # Extract endpoint path from request URL endpoint_path = extract_path(endpoint) ttl = IDEMPOTENCY_TTL.get(endpoint_path, 60 * 60) # Default 1 hour redis_client.setex(f"idempotent:{key}", ttl, json.dumps(result))
5. Implement Comprehensive Logging
Track idempotency-related events for debugging and monitoring:
// Java Spring Boot REST API logging example @Aspect @Component public class IdempotencyLoggingAspect { private final Logger logger = LoggerFactory.getLogger(IdempotencyLoggingAspect.class); @Around("@annotation(org.springframework.web.bind.annotation.PostMapping)") public Object logIdempotencyChecks(ProceedingJoinPoint joinPoint) throws Throwable { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); String idempotencyKey = request.getHeader("Idempotency-Key"); String endpoint = request.getRequestURI(); if (idempotencyKey != null) { logger.info("Idempotency check started for request", Map.of( "idempotencyKey", idempotencyKey, "endpoint", endpoint, "method", request.getMethod() )); boolean isDuplicate = idempotencyService.checkIfDuplicate(idempotencyKey); logger.info("Idempotency check result", Map.of( "idempotencyKey", idempotencyKey, "endpoint", endpoint, "isDuplicate", String.valueOf(isDuplicate) )); if (isDuplicate) { Object cachedResponse = idempotencyService.getCachedResponse(idempotencyKey); logger.info("Returning cached response for duplicate request", Map.of( "idempotencyKey", idempotencyKey )); return cachedResponse; } } Object result = joinPoint.proceed(); if (idempotencyKey != null) { idempotencyService.storeResponse(idempotencyKey, result); logger.info("Stored response for idempotent request", Map.of( "idempotencyKey", idempotencyKey )); } return result; } }
6. Handle Partial Failures in REST API Transactions
Ensure consistency in the face of failures:
// Java Spring Boot REST API with transaction management @Service public class PaymentService { private final PaymentRepository paymentRepository; private final PaymentGatewayClient paymentGateway; @Autowired public PaymentService(PaymentRepository paymentRepository, PaymentGatewayClient paymentGateway) { this.paymentRepository = paymentRepository; this.paymentGateway = paymentGateway; } @Transactional public PaymentResponse processPayment(String idempotencyKey, PaymentRequest request) { // Check for existing payment with this idempotency key Optional<Payment> existingPayment = paymentRepository.findByIdempotencyKey(idempotencyKey); if (existingPayment.isPresent()) { return mapToResponse(existingPayment.get()); } // Create payment record in PENDING state Payment payment = new Payment(); payment.setIdempotencyKey(idempotencyKey); payment.setAmount(request.getAmount()); payment.setStatus(PaymentStatus.PENDING); paymentRepository.save(payment); try { // Call payment gateway PaymentGatewayResponse gatewayResponse = paymentGateway.charge(request); // Update payment record payment.setGatewayReference(gatewayResponse.getTransactionId()); payment.setStatus(PaymentStatus.COMPLETED); paymentRepository.save(payment); return mapToResponse(payment); } catch (Exception e) { // Mark as failed payment.setStatus(PaymentStatus.FAILED); payment.setFailureReason(e.getMessage()); paymentRepository.save(payment); throw new PaymentProcessingException("Payment failed", e); } } private PaymentResponse mapToResponse(Payment payment) { PaymentResponse response = new PaymentResponse(); response.setPaymentId(payment.getId()); response.setStatus(payment.getStatus().toString()); response.setAmount(payment.getAmount()); response.setCreatedAt(payment.getCreatedAt()); return response; } }
Choosing the Right Strategy for Your REST API
Select your idempotency implementation based on these considerations:
Strategy | Best For | Complexity | Scalability |
---|---|---|---|
Database Constraints | Simple, low-volume REST APIs | Low | Medium |
In-Memory Tracking | Single-instance REST API services | Low | Low |
Redis Cache | Distributed microservice REST APIs | Medium | High |
Message Brokers | Event-driven architectures with REST API entry points | Medium | High |
For complex systems, consider combining strategies. For example:
- Use Redis for initial REST API request idempotency
- Use message broker duplicate detection for asynchronous processing
- Use database constraints as a final safety mechanism
Summary
Idempotency is a critical property for building reliable REST APIs, especially in distributed systems where retries and network failures are common. The key points to remember:
- Idempotent operations produce the same result whether executed once or multiple times
- HTTP methods GET, PUT, DELETE, and HEAD are inherently idempotent; POST can be made idempotent with custom implementations
- Multiple implementation strategies exist, each with trade-offs:
- Database unique constraints
- In-memory tracking
- Distributed caching with Redis
- Message broker duplicate detection
- Follow best practices like standardizing headers, generating appropriate identifiers, documenting behavior, setting TTLs, implementing logging, and handling partial failures
By implementing idempotency in your REST APIs, you can create more robust systems that gracefully handle the realities of distributed computing and network uncertainty.
References
-
RESTful API Design Resources:
- Fielding, Roy Thomas. "Architectural Styles and the Design of Network-based Software Architectures" (2000)
- Richardson, Leonard; Ruby, Sam. "RESTful Web Services" (2007)
-
Microsoft Learn: Enable duplicate detection in Azure Service Bus
-
Redis Documentation: Using Redis for Idempotent API Requests
-
PostgreSQL Documentation: Unique Constraints
-
Stripe API Documentation: Idempotent Requests
-
PayPal REST API Documentation: Idempotency
-
Martin Fowler: Idempotent Receiver Pattern