Networks fail. Clients retry. Without protection, a retried POST /payments charges the customer twice.
The idea
The client sends a unique Idempotency-Key header with each logical request. The server stores the key with the response. If the same key arrives again, the server returns the stored response instead of running the operation.
Minimal flow
- Client generates a UUID and sends it with the request.
- Server checks the key in a store (a DB table with a unique index works).
- Key not found: run the operation, save key and response, return it.
- Key found: return the saved response.
CREATE TABLE idempotency_keys (
key text PRIMARY KEY,
response jsonb NOT NULL,
created_at timestamptz DEFAULT now()
);
sequenceDiagram
Client->>Server: POST /payments (Idempotency-Key: abc)
Server->>Store: key abc exists?
Store-->>Server: no
Server->>Server: charge card, save response
Server-->>Client: 201 Created
Client->>Server: retry with same key
Server->>Store: key abc exists?
Store-->>Server: yes, stored response
Server-->>Client: 201 Created (no second charge)
Pitfalls
- Insert the key and run the operation in one transaction, or two concurrent retries both pass the check.
- Reject a reused key sent with a different payload.
- Expire old keys (24 hours is common).