27.5. Idempotency
Table of Contents
Why Idempotency Matters in APIs
Idempotency is about making repeated API calls safe. In real systems, clients can retry requests if:
- The network is unstable
- A timeout occurs
- The client crashes and restarts
- A load balancer or proxy retries a request
If your API is not idempotent, a single user action might accidentally create multiple records or perform the same operation multiple times, such as:
- Charging a credit card twice
- Creating duplicate orders
- Sending the same email many times
Idempotency helps you guarantee that repeating the same operation has the same effect as doing it once.
Idempotency rule
An operation is idempotent if performing it once or many times produces the same effect on the system state and the same kind of response to the client.
This is about the effect on the server, not about how many times the client sends the request.
Idempotent vs Non‑Idempotent HTTP Methods
HTTP itself classifies methods by whether they are supposed to be idempotent.
| HTTP Method | Intended Idempotency | Typical Use |
|---|---|---|
| GET | Idempotent | Read a resource |
| HEAD | Idempotent | Read only headers of a resource |
| OPTIONS | Idempotent | Discover server capabilities |
| PUT | Idempotent | Replace or create a resource at a URL |
| DELETE | Idempotent | Remove a resource |
| POST | Non‑idempotent | Create or perform actions / side effects |
| PATCH | Not specified | Partially update a resource |
Examples of naturally idempotent methods
GET
GET /users/123 HTTP/1.1
Host: api.example.com
Calling this 1 time or 100 times does not change the user. It just reads data.
If implemented correctly, GET is idempotent.
PUT
PUT /users/123 HTTP/1.1
Content-Type: application/json
{
"name": "Alice",
"email": "alice@example.com"
}If your PUT logic is:
- If user 123 exists, replace their data with this payload
- If not, create user 123 with this data
then calling the same PUT multiple times leaves the user in the same final state. So PUT is idempotent by design.
DELETE
DELETE /users/123 HTTP/1.1If you implement:
- First call: user 123 is deleted, returns
204 No Content - Next calls: user 123 no longer exists, maybe returns
404 Not Found
The resource state (user 123 is gone) is the same after each call, so DELETE is idempotent, even if the responses differ.
POST is not idempotent by default
POST /orders HTTP/1.1
Content-Type: application/json
{
"product_id": 10,
"quantity": 1
}
If your logic is: “create a new order,” then calling POST /orders twice creates two orders. This is not idempotent, because the state changes each time.
Most payment APIs, order creation APIs, and “do this action” endpoints use POST and must be carefully designed if you want safe retries.
Idempotency in Practice: Real‑World Scenarios
Example: Payment or charge endpoint
Imagine an endpoint:
POST /payments
{
"user_id": 123,
"amount": 50.00
}If the client sends the request, gets a timeout, and retries, you might charge the card twice. This is a serious problem.
With idempotency, you want this behavior:
- First
POST /paymentswith some unique idempotency key: charge is created. - Client retries the exact same request with the same idempotency key:
- The server does not create a second charge,
- It returns the same result as the first time.
Example: Idempotent update of a resource
For an update endpoint:
PUT /users/123/email
{
"email": "new@example.com"
}
Even without a special key, if you always set the email to this value, this is naturally idempotent.
Retries are safe, because the email is the same after each call.
Implementing Idempotency with Idempotency Keys
Idempotency keys are the most common way to make non‑idempotent operations (usually POSTs) behave idempotently.
Basic idea
- The client generates a unique key for a “logical operation”
- It sends this key with the request, often in a header, for example:
Idempotency-Key: 8a0a0b16-49a3-4cfc-bf43-785f9c330123- The server records that key and the result of processing the request
- If the same key appears again with the same operation, the server returns the saved result instead of performing it again
Idempotency key rule
For a given idempotency key, your server must:
- Perform the operation at most once, and
- Return consistent responses for every request that uses this key, as long as the request details match.
Where to put the key
Common approaches:
| Location | Example |
|---|---|
| HTTP header | Idempotency-Key: 123e4567-e89b-12d3-a456-426614174000 |
| Request body field | { "idempotency_key": "..." } |
| Query parameter | POST /payments?idempotency_key=... |
Headers are the most common choice.
Server‑side storage
To support idempotency, the server needs a data store that maps:
| Field | Description |
|---|---|
| idempotency_key | Unique key provided by client |
| request_hash | Hash or snapshot of the request data |
| response_status | HTTP status code returned the first time |
| response_body | Response body returned the first time |
| created_at | When this record was stored |
| state | e.g. processing, succeeded, failed |
You can store this in:
- A relational table (PostgreSQL)
- A key value store (Redis)
Example in a relational table:
CREATE TABLE idempotency_keys (
key TEXT PRIMARY KEY,
request_hash TEXT NOT NULL,
response_status INTEGER,
response_body JSONB,
state TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);Basic processing flow
- Receive request with
Idempotency-KeyK and body B. - Compute a simple request hash H from B (for example
SHA256(B)or a stable JSON hash). - Look up
Kin the idempotency store.
Case 1: No record found
You have never seen this key.
- Insert record:
(K, H, state = "processing"). - Perform business logic (for example create payment).
- Store
(response_status, response_body, state = "succeeded"). - Return the response.
Case 2: Record exists and `state = "succeeded"`
- Compare current request hash H with stored request_hash.
- If they are the same, return the stored response.
- If they are different, treat this as a client error: the client is misusing the key.
Example:
HTTP/1.1 409 Conflict
Content-Type: application/json
{
"error": "Idempotency key reuse with different request body"
}Case 3: Record exists and `state = "processing"`
This may happen if:
- A previous request is still being processed, or
- A previous process crashed before updating state to
succeededorfailed.
You have options:
- Return
409 Conflictto tell the client that the operation is in progress. - Wait for a short period and then recheck the state.
- Implement a timeout and then treat it as an error.
Examples of Idempotent Implementations
Example: Create order endpoint with idempotency key
Assume this endpoint:
POST /orders
Idempotency-Key: 50b3d1f4-07b0-4fbb-9c4c-4e1f7f4a0a10
Content-Type: application/json
{
"user_id": 123,
"items": [
{ "product_id": 10, "quantity": 1 }
]
}First request
- Key not found, so you:
- Insert record
(key, hash, state="processing"). - Create an order in the database, get
order_id = 101. - Store
response_status = 201,response_body = {"order_id": 101, ...},state="succeeded". - Return
201 Createdwithorder_id = 101.
Second identical request (retry)
- Key exists,
state="succeeded", request hash matches. - Do not create a new order.
- Return the stored response:
HTTP/1.1 201 Created
Content-Type: application/json
{
"order_id": 101,
"status": "pending",
...
}To the client this looks as if the operation was processed once, even though it was retried.
Idempotency, Concurrency, and Database Design
Idempotency is tied closely to database constraints and atomic operations.
Using unique constraints
Sometimes you can enforce idempotency without explicit idempotency keys, by using unique fields.
For example:
- Each payment has a
client_transaction_idthat must be unique. - The client sets this ID for each logical payment.
In SQL:
CREATE TABLE payments (
id SERIAL PRIMARY KEY,
client_transaction_id TEXT UNIQUE NOT NULL,
user_id INTEGER NOT NULL,
amount NUMERIC(10,2) NOT NULL
);Then, in your API:
- Client sends:
POST /payments
{
"client_transaction_id": "abc-123",
"user_id": 42,
"amount": 50.00
}- Server tries to
INSERTintopayments. - If the same request is retried:
- The
INSERTfails becauseclient_transaction_idis already used. - You can then
SELECTand return the existing payment instead of creating a new one.
This pattern uses database uniqueness to avoid duplicates.
Atomic check‑then‑insert
To avoid race conditions you must:
- Do the check and insert in a single atomic database operation or transaction.
- Avoid patterns like:
- “Check if record exists” in one query, then
- “Insert” in a second query without locking.
Otherwise two concurrent requests can both think the record does not exist and both insert, which breaks idempotency.
Use:
INSERT ... ON CONFLICT DO NOTHINGorINSERT ... ON CONFLICT ... DO UPDATE- Or transactions with locks, depending on the database.
Idempotent APIs vs Idempotent Handlers
There is a subtle distinction:
- An idempotent HTTP method means that, by protocol definition, the method should be idempotent (like GET, PUT, DELETE).
- An idempotent handler or endpoint means your implementation has idempotent behavior, even if the method is normally non‑idempotent.
You can have:
- A non‑idempotent GET if you implement it incorrectly (for example, each GET increments a counter in the database).
- An idempotent POST if you implement idempotency keys properly and protect against duplicates.
So, you must think about behavior, not just the HTTP verb.
Idempotency and Response Semantics
Idempotent operations do not have to:
- Always return identical HTTP status codes
- Always return byte‑for‑byte identical bodies
They must:
- Leave the resource in the same final state
- Give a consistent semantic result for the same logical operation
For example, a DELETE endpoint:
- First call:
204 No Content - Second call:
404 Not Found
The resource is deleted in both cases. The operation is idempotent, even though the status and body differ.
For a POST with idempotency keys you usually design so that:
- Repeat requests with the same key and same body return exactly the same status and body.
- This makes client behavior simpler.
Choosing When to Use Idempotency
You do not need idempotency keys everywhere. Focus on:
- Actions that must not be executed multiple times, such as:
- Payments or charges
- Order creation
- Critical state changes (for example upgrade a subscription)
- Situations where retries are likely:
- Mobile clients on unstable networks
- Long running operations that may time out
- APIs behind proxies or gateways with automatic retry logic
For simple reads or simple updates that are already idempotent (like PUT that overwrites data), you may not need special keys.
Common Pitfalls and Best Practices
Pitfalls
- Ignoring request differences for the same key
- If you accept a different body for the same idempotency key, behavior becomes undefined.
- Always compare a hash and reject mismatches.
- Not persisting idempotency data long enough
- If you delete idempotency records too quickly, retries may arrive after deletion, and you will reprocess the operation.
- Only checking at the application layer
- Without database constraints, a restart or race condition can still create duplicates.
- Combine app logic with database uniqueness where possible.
- Side effects outside your database
- For example, calling an external payment provider twice, or sending emails twice, even if your local DB is idempotent.
- You may need idempotency keys when talking to external systems too.
Best practices
- Use idempotency keys for non‑idempotent POSTs that represent important business operations.
- Store and compare request hashes to detect misuse of idempotency keys.
- Use database unique constraints for natural unique fields.
- Make idempotency key storage transactional with your main operation as much as possible.
- Define a reasonable TTL (time to live) for idempotency records, for example 24 hours or 7 days.
- Document your idempotency behavior in your API documentation, including:
- How to generate keys
- Which endpoints support them
- How long keys are valid
- What happens on conflicting use of a key
Summary
- Idempotency means one or many identical requests have the same effect on the system.
- Some HTTP methods are designed to be idempotent (GET, PUT, DELETE), but implementation matters.
- POST is not idempotent by default, but you can make it idempotent with idempotency keys and proper storage.
- Use unique keys, database constraints, and atomic operations to prevent duplicate processing.
- Idempotency is essential for retries, payment systems, order creation, and any critical operation in real‑world backends.
Views: 8
KAHIBARO