Idempotency means that making the same request multiple times produces the same result as making it once. This is critical for API reliability - if a network timeout occurs, clients can safely retry without causing duplicate actions.
Idempotency
A property where making the same API request multiple times produces the same result.
Why It Matters
Network failures happen: Request sent, server processed it, but response was lost. Client doesn't know if it worked. Should they retry?
Without idempotency: Retry creates duplicate order, double charge, or duplicate record.
With idempotency: Retry returns same result as original request. Safe to retry any failed request.
HTTP Methods and Idempotency
| Method | Idempotent | Safe | Description |
|---|---|---|---|
| GET | Yes | Yes | Reading data never changes it |
| HEAD | Yes | Yes | Same as GET, no body |
| OPTIONS | Yes | Yes | Returns allowed methods |
| PUT | Yes | No | Replace entire resource |
| DELETE | Yes | No | Delete returns same state |
| POST | No | No | Creates new resource each time |
| PATCH | Depends | No | Depends on implementation |
Safe = doesn't modify server state Idempotent = multiple identical requests = same result
Implementing Idempotency
Idempotency key: Client generates unique key (UUID) and sends with request. Server stores key with result. On retry, server returns cached result instead of re-processing.
How it works:
- Client generates idempotency key
- Client sends request with key in header
- Server checks if key exists
- If exists: return stored result
- If new: process request, store result with key
- Return result
Idempotency Key Best Practices
Key format: Use UUID v4 or combine user ID + timestamp + random. Must be unique per distinct operation.
Key lifetime: Store keys for 24-48 hours. Too short = retries fail. Too long = storage grows.
Key scope: Keys should be scoped to user/account. Different users can use same key.
Response storage: Store full response (status code, body) to return on retry.
Making Operations Idempotent
Create operations (POST):
Use idempotency keys. Or use PUT with client-generated ID: PUT /orders/uuid-123
Update operations:
Use conditional updates. UPDATE WHERE id = 1 AND version = 5 fails if version changed.
Financial operations: Store transaction IDs. Check if transaction already processed before executing.
Increment operations:
Instead of balance += 10, use SET balance = 110 WHERE balance = 100 (check current value).
Common Patterns
Optimistic locking: Include version number. Update only if version matches. Prevents concurrent modification.
Unique constraints: Database-level uniqueness prevents duplicates even under race conditions.
Request deduplication: Hash request body + timestamp window. Reject duplicates within window.
Two-phase operations:
- Reserve/prepare (idempotent)
- Commit/execute (idempotent with reference to step 1)
Real-World Examples
Payment processing: Stripe requires idempotency key for charges. Retry same key = same result, no double charge.
Order creation: Store order with unique reference. Retry creates no duplicate if reference exists.
Email sending: Store sent message IDs. Skip if already sent.
Common Mistakes
1. Generating key server-side: Client must generate key. Server-generated key changes on retry.
2. Short key expiration: Network issues can last hours. Keep keys at least 24 hours.
3. Not storing full response: Store status code and body. Returning just "already processed" doesn't help client.
4. Ignoring partial failures: If request partially succeeded, retry logic must handle incomplete state.
5. Using POST for idempotent operations: If operation is naturally idempotent, use PUT or PATCH instead.
When Idempotency Matters Most
- Payment and financial transactions
- Order processing
- Any operation that can't be easily reversed
- Operations called from unreliable networks
- Webhook handlers (may be called multiple times)
Code Examples
Implementing Idempotency Keys
// Middleware for idempotency key handling
const idempotencyMiddleware = async (req, res, next) => {
const key = req.headers['idempotency-key'];
if (!key) {
return next(); // No key, process normally
}
// Check for existing result
const cached = await redis.get(`idempotency:${req.user.id}:${key}`);
if (cached) {
const { statusCode, body } = JSON.parse(cached);
return res.status(statusCode).json(body);
}
// Store original res.json to intercept response
const originalJson = res.json.bind(res);
res.json = async (body) => {
// Cache the response for 24 hours
await redis.setex(
`idempotency:${req.user.id}:${key}`,
86400,
JSON.stringify({ statusCode: res.statusCode, body })
);
return originalJson(body);
};
next();
};
// Client-side usage
async function createOrder(orderData) {
const idempotencyKey = crypto.randomUUID();
const response = await fetch('/api/orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey
},
body: JSON.stringify(orderData)
});
return response.json();
}Related Terms
CORS
Cross-Origin Resource Sharing - A security mechanism that controls how web pages can request resources from different domains.
Server-Sent Events (SSE)
A standard for pushing real-time updates from server to client over HTTP.
HTTP Status Codes
Standard response codes that indicate the result of an HTTP request.
REST API
An architectural style for building web APIs using HTTP methods and stateless communication.