python 51 lines · 9 steps

Idempotent payment endpoints in FastAPI

How Redis locks and cached results make a payment POST safe to retry without double-charging.

Explained by highlit
1import json
2import hashlib
3from datetime import timedelta
4 
5import redis.asyncio as redis
6from fastapi import APIRouter, Header, HTTPException, Request, Response, status
7 
8router = APIRouter()
9rdb = redis.Redis(host="localhost", decode_responses=True)
10 
11LOCK_TTL = 30
12RESULT_TTL = timedelta(hours=24)
13 
14 
15def _fingerprint(method: str, path: str, body: bytes) -> str:
16 return hashlib.sha256(method.encode() + path.encode() + body).hexdigest()
17 
18 
19@router.post("/payments", status_code=status.HTTP_201_CREATED)
20async def create_payment(
21 request: Request,
22 idempotency_key: str = Header(..., alias="Idempotency-Key"),
23):
24 body = await request.body()
25 fp = _fingerprint(request.method, request.url.path, body)
26 lock_key = f"idem:lock:{idempotency_key}"
27 result_key = f"idem:result:{idempotency_key}"
28 
29 cached = await rdb.get(result_key)
30 if cached is not None:
31 stored = json.loads(cached)
32 if stored["fingerprint"] != fp:
33 raise HTTPException(status.HTTP_422_UNPROCESSABLE_ENTITY, "Key reused with different payload")
34 return Response(content=stored["body"], status_code=stored["status"], media_type="application/json")
35 
36 acquired = await rdb.set(lock_key, fp, nx=True, ex=LOCK_TTL)
37 if not acquired:
38 raise HTTPException(status.HTTP_409_CONFLICT, "Request with this Idempotency-Key is in progress")
39 
40 try:
41 payload = json.loads(body)
42 payment = await charge_payment(payload)
43 response_body = json.dumps(payment)
44 await rdb.set(
45 result_key,
46 json.dumps({"fingerprint": fp, "status": 201, "body": response_body}),
47 ex=RESULT_TTL,
48 )
49 return Response(content=response_body, status_code=201, media_type="application/json")
50 finally:
51 await rdb.delete(lock_key)
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1An idempotency key plus a stored request fingerprint lets you safely deduplicate retries while rejecting key reuse with a different payload.
  2. 2A short-TTL Redis lock set with NX prevents two concurrent requests from both performing the side effect.
  3. 3Always release the in-progress lock in a finally block so a failure doesn't wedge the key forever.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Idempotent payment endpoints in FastAPI — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code