API Idempotency: Why It Matters and How to Implement It
API Idempotency: Why It Matters and How to Implement It
A customer clicks "Pay Now." The request times out. Did the payment go through? The customer clicks again. Without idempotency, they get charged twice. With idempotency, the second request returns the same result as the first — no duplicate charge.
Idempotency means: making the same request multiple times produces the same result as making it once.
Which HTTP Methods Are Idempotent?
| Method | Idempotent? | Why |
|---|---|---|
| GET | ✅ Yes | Reading data doesn't change state |
| PUT | ✅ Yes | Replacing a resource with the same data = same result |
| DELETE | ✅ Yes | Deleting an already-deleted resource = same result |
| PATCH | ❌ No | Partial updates may produce different results (e.g., increment) |
| POST | ❌ No | Creating a resource twice = two resources |
The problem: POST and PATCH are not naturally idempotent. But POST is used for the most critical operations — payments, orders, account creation.
The Idempotency Key Pattern
Stripe popularized this pattern. The client generates a unique key per operation and includes it in the request header.
Request:
POST /api/payments
Idempotency-Key: idem_abc123def456
Content-Type: application/json
{
"amount": 4999,
"currency": "usd",
"customer": "cus_123"
}
Server behavior:
- First request: Process the payment, store the result keyed by
idem_abc123def456, return the result - Second request (same key): Look up the stored result, return it without reprocessing
- Different key: Process as a new payment
Implementation Steps
- Client generates a unique key — UUID v4 is the standard choice
- Server receives the request — check if this idempotency key exists in the store
- Key not found — process the request, store
{key, status_code, response_body, created_at} - Key found, processing — return 409 Conflict (request is still being processed)
- Key found, completed — return the stored response (same status code and body)
- Key found, different request body — return 422 (can't reuse a key for a different request)
Storage Requirements
| Field | Purpose |
|---|---|
idempotency_key | The unique key (primary key) |
request_hash | Hash of the request body (detect misuse) |
status_code | Stored HTTP response code |
response_body | Stored response JSON |
status | processing or completed |
created_at | When the key was first used |
expires_at | When to garbage collect (24-48 hours) |
Key Expiration
Idempotency keys should expire after 24-48 hours. After that, the same key can be reused. This prevents unbounded storage growth while covering retry windows.
When to Implement Idempotency
Always implement for:
- Payment processing (charges, refunds, transfers)
- Account creation
- Order placement
- Any operation where duplication has financial or data consequences
Optional for:
- Update operations (PUT is already idempotent)
- Read operations (GET is already idempotent)
- Delete operations (DELETE is already idempotent)
- Operations where duplicates are harmless (sending a notification twice is annoying but not catastrophic)
How Stripe Does It
Stripe accepts Idempotency-Key header on all POST requests:
POST /v1/charges
Idempotency-Key: idem_abc123
Authorization: Bearer sk_live_...
- Keys expire after 24 hours
- Replayed requests return the original response
- Using the same key with different parameters returns an error
- Keys are scoped to the API key (different accounts can use the same key)
How to Generate Keys
Client-Side
UUID v4: "550e8400-e29b-41d4-a716-446655440000"
Use UUID v4. It's universally supported, has negligible collision probability, and requires no coordination.
Alternative: Deterministic keys — hash the operation parameters to generate the key. hash(user_id + amount + currency + timestamp). This way, retrying the same operation automatically uses the same key without the client storing it.
Common Mistakes
| Mistake | Impact | Fix |
|---|---|---|
| No idempotency on payments | Duplicate charges | Idempotency key on all POST endpoints |
| Server-generated keys | Client can't retry safely | Client generates keys |
| No key expiration | Unbounded storage growth | Expire after 24-48 hours |
| Processing not locked | Race condition — two workers process same key | Lock on key before processing |
| Same key, different request allowed | Ambiguous behavior | Return error if request body differs |
| Key scoped globally | Cross-tenant key collision | Scope to API key or tenant |
Beyond Idempotency: At-Least-Once vs Exactly-Once
- At-most-once: Fire and forget. Request may not be processed. (No retries.)
- At-least-once: Retry until success. Request may be processed multiple times. (Retries without idempotency.)
- Exactly-once: Process exactly once. (Retries + idempotency.)
Most APIs should aim for at-least-once delivery with idempotent handlers, achieving effectively exactly-once processing.
Designing reliable APIs? Explore API best practices and tools on APIScout — architecture guides, comparisons, and developer resources.