| name | idempotent-api |
| description | Design idempotent APIs that handle duplicate requests safely. Outputs idempotency key strategy, deduplication storage design, response caching patterns, and client retry guidance. |
| argument-hint | ["API type","expected retry rate","storage backend","consistency requirements"] |
| allowed-tools | Read, Write |
Idempotent API Design
An idempotent operation produces the same result whether executed once or many times. HTTP GET, PUT, and DELETE are naturally idempotent. POST is not — but can be made so with idempotency keys. This matters because networks fail, clients retry, and without idempotency, retries create duplicate charges, double sends, and data corruption.
Process
- Identify non-idempotent endpoints. POST (create), financial operations, email sends, state transitions.
- Design the idempotency key. Client-generated UUID per logical operation. Scoped to the user/account.
- Choose deduplication storage. Redis (fast, TTL-based) or database (durable, auditable).
- Define the response strategy. Return the original response on duplicate — never re-execute.
- Set key TTL. 24 hours is standard for most operations; longer for financial.
- Document for API consumers. How to generate keys, when to use them, retry guidance.
Implementation
fastapi FastAPI, Header, HTTPException, Depends
typing
redis.asyncio redis
json
hashlib
datetime datetime
app = FastAPI()
r = redis.Redis(host=, port=, decode_responses=)
IDEMPOTENCY_TTL =
() -> []:
idempotency_key:
():
scoped_key:
cached = r.get(scoped_key)
cached:
stored = json.loads(cached)
fastapi.responses JSONResponse
JSONResponse(
status_code=stored[],
content=stored[],
headers={: },
)
order = order_service.create(request)
response_body = {: order., : order.status}
scoped_key:
r.setex(
scoped_key,
IDEMPOTENCY_TTL,
json.dumps({: , : response_body}),
)
response_body
():
scoped_key:
cached = r.get(scoped_key)
cached:
json.loads(cached)[]
order = order_service.get(order_id)
order order.customer_id != user_id:
HTTPException()
order.status == :
result = {: order_id, : , : }
order.status == :
order = order_service.confirm(order_id)
result = {: order_id, : }
:
HTTPException(, )
scoped_key:
r.setex(scoped_key, IDEMPOTENCY_TTL, json.dumps({: result}))
result