python 57 lines · 7 steps

Building an idempotency decorator in Flask

A Redis-backed decorator that makes Flask endpoints safe to retry by caching and replaying responses keyed on an Idempotency-Key header.

Explained by highlit
1import hashlib
2import json
3from functools import wraps
4from flask import g, request, jsonify, current_app
5 
6 
7def idempotent(ttl=86400):
8 def decorator(view):
9 @wraps(view)
10 def wrapper(*args, **kwargs):
11 key = request.headers.get("Idempotency-Key")
12 if not key:
13 return jsonify(error="Idempotency-Key header required"), 400
14 
15 body_hash = hashlib.sha256(request.get_data() or b"").hexdigest()
16 g.idempotency_key = key
17 cache = current_app.extensions["redis"]
18 cache_key = f"idem:{request.endpoint}:{key}"
19 
20 record = cache.get(cache_key)
21 if record is not None:
22 stored = json.loads(record)
23 if stored["body"] != body_hash:
24 return jsonify(error="Idempotency-Key reused with different payload"), 422
25 if stored["status"] == "pending":
26 return jsonify(error="Request already in progress"), 409
27 return current_app.response_class(
28 response=stored["response"],
29 status=stored["code"],
30 mimetype="application/json",
31 )
32 
33 lock = cache.set(
34 cache_key,
35 json.dumps({"status": "pending", "body": body_hash}),
36 nx=True,
37 ex=ttl,
38 )
39 if not lock:
40 return jsonify(error="Request already in progress"), 409
41 
42 response = current_app.make_response(view(*args, **kwargs))
43 cache.set(
44 cache_key,
45 json.dumps({
46 "status": "done",
47 "body": body_hash,
48 "code": response.status_code,
49 "response": response.get_data(as_text=True),
50 }),
51 ex=ttl,
52 )
53 return response
54 
55 return wrapper
56 
57 return decorator
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Idempotency keys let clients retry safely by returning the original response instead of re-executing side effects.
  2. 2Hashing the request body detects a reused key with a mismatched payload, preventing accidental cross-request collisions.
  3. 3An atomic set-if-not-exists acts as a lock so concurrent duplicate requests can't both run the view.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Building an idempotency decorator in Flask — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code