1. What is Idempotency?

A mathematical function f is idempotent if f(f(x)) = f(x) — applying it multiple times produces the same result as applying it once. In APIs, an operation is idempotent if making the same request N times has the same effect on server state as making it once.

This matters because distributed systems have three outcomes for any request: success, failure, or unknown. A network timeout does not tell you whether the server processed the request. Without idempotency, you cannot safely retry — you might create duplicate orders, duplicate charges, or duplicate records.

2. HTTP Method Idempotency

HTTP MethodIdempotent?Safe (read-only)?Notes
GETYesYesRead-only, no state change
HEADYesYesSame as GET but no response body
OPTIONSYesYesReturns allowed methods, no state change
PUTYesNoSets resource to specific state; repeating has same result
DELETEYesNoFirst call deletes; subsequent calls return 404 but state unchanged
POSTNo (by default)NoEach call may create a new resource; requires idempotency key to make safe
PATCHNo (by default)NoIncremental update; repeating may compound (e.g. increment counter twice)

DELETE Idempotency Nuance

DELETE is idempotent in terms of server state — after N calls, the resource is deleted. But the HTTP response code changes: the first call returns 200 or 204, subsequent calls return 404. Some strict REST purists argue this breaks idempotency. In practice, clients should treat 404 on DELETE as success — the desired state (resource does not exist) has been achieved.

3. The Problem: Unsafe POST Retries

POST is not idempotent by design. Consider a payment creation API:

  Client  ─── POST /payments {amount: 100, card: xxxx} ───▶  Server
                                                               │
                                                         DB write succeeds
                                                         Payment charged
                                                               │
                                                         Network timeout!
                                                               │
  Client  <─── ??? (never received 200 response)        Response lost

  Client retries:
  Client  ─── POST /payments {amount: 100, card: xxxx} ───▶  Server
                                                         NEW payment created
                                                         Customer charged TWICE!

The client has no way to know if the first request succeeded. If it retries naively, the customer is charged twice. Idempotency keys solve this.

4. Idempotency Keys

An idempotency key is a unique identifier generated by the client before making the request. The client includes it in the request header. The server stores the key along with the result. On a retry with the same key, the server returns the stored result without re-executing the operation.

  Client generates: idempotency_key = UUID("a3f8-bc12-...")

  First request:
  Client  ──  POST /payments
              Idempotency-Key: a3f8-bc12-...
              {amount: 100}              ──▶  Server processes, stores:
                                              key a3f8-bc12 → {payment_id: 789, status: success}
                                              Returns: 200 {payment_id: 789}

  Network timeout — client retries with SAME key:
  Client  ──  POST /payments
              Idempotency-Key: a3f8-bc12-...
              {amount: 100}              ──▶  Server: key a3f8-bc12 already exists!
                                              Returns CACHED result: 200 {payment_id: 789}
                                              NO new payment created ✓

5. Server-Side Idempotency Key Implementation

PHP — Idempotency key middleware with Redis function handleWithIdempotency(string $key, callable $handler): array { $redis = new Redis(); $redis->connect('127.0.0.1', 6379); $cacheKey = "idempotency:{$key}"; // Check if key already processed $stored = $redis->get($cacheKey); if ($stored !== false) { return json_decode($stored, true); // return cached result } // Acquire processing lock to prevent concurrent duplicate requests $lock = $redis->set("lock:{$cacheKey}", 1, ['nx', 'ex' => 30]); if (!$lock) { // Another request with same key is in-flight — wait briefly usleep(200000); $stored = $redis->get($cacheKey); return $stored ? json_decode($stored, true) : ['error' => 'concurrent_request']; } // Execute the actual operation $result = $handler(); // Store result with 24-hour TTL $redis->setex($cacheKey, 86400, json_encode($result)); $redis->del("lock:{$cacheKey}"); return $result; } // Usage in payment endpoint: $key = $_SERVER['HTTP_IDEMPOTENCY_KEY'] ?? null; if (!$key) { http_response_code(400); exit('Idempotency-Key header required'); } $result = handleWithIdempotency($key, function() { return createPayment($_POST['amount'], $_POST['card_token']); }); echo json_encode($result);

6. Stripe's Idempotency-Key Header

Stripe is the industry benchmark for payment API idempotency. Every POST request to the Stripe API accepts an Idempotency-Key header. Stripe stores the key and result for 24 hours. Key behaviours:

  • Keys are scoped to your API key — two different accounts can use the same UUID without conflict
  • If you send the same key with different request body, Stripe returns 422 (the key is locked to the original request parameters)
  • Keys expire after 24 hours — after that, the same UUID can be reused
  • Stripe recommends using UUID v4 generated fresh for each distinct operation
  • Stripe returns the original HTTP status code and response body on a replay, including error responses — if the original request failed, the cached failure is returned

7. Database Upsert Pattern for Idempotent Writes

For idempotent database writes, use a natural unique key or idempotency token with an upsert (insert-or-ignore / insert-on-conflict-do-nothing):

SQL — idempotent order creation with unique token CREATE TABLE orders ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, client_token VARCHAR(64) NOT NULL, -- client-generated UUID user_id BIGINT UNSIGNED NOT NULL, amount_cents INT NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'pending', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_client_token (client_token) -- ensures idempotency ); -- First attempt: INSERT INTO orders (client_token, user_id, amount_cents) VALUES ('a3f8-bc12-...', 42, 9999); -- Inserts row, returns new order id -- Retry with same token: INSERT INTO orders (client_token, user_id, amount_cents) VALUES ('a3f8-bc12-...', 42, 9999) ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id); -- No new row created, returns existing row id ✓

Idempotency Key Scope Matters

Idempotency keys must be scoped to a specific operation. Never reuse the same key for different operations. A key that worked for "create payment" should never be reused for "create refund." The server must also validate that the request body for a retried key matches the original — Stripe rejects requests where the key matches but the body differs, preventing accidental misuse of a key from a previous operation.

8. Making Non-Idempotent Operations Idempotent

You can retrofit idempotency onto any operation with the following pattern:

  1. Require clients to send a unique Idempotency-Key header on all write operations
  2. Before executing, check if the key exists in your store (Redis or DB table)
  3. If the key exists: return the stored result (do not re-execute)
  4. If the key is new: execute the operation, store the result with the key, return the result
  5. Handle concurrent requests for the same key with a distributed lock
  6. Expire keys after a reasonable window (24 hours to 7 days)

This pattern is applicable to payment creation, order placement, email sending, SMS dispatch, and any operation that must not be duplicated on retry.

How We Research and Update This Guide

We test the underlying formula or workflow, compare outputs with reliable references, and revise examples whenever the page content changes.

  • The workflow or formula is tested directly in the tool and compared against independent reference examples.
  • Examples are kept practical so readers can verify the result without hidden assumptions.
  • Pages are revised whenever the interface, calculation flow, or surrounding guidance materially changes.

Frequently Asked Questions — Idempotent APIs