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
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Idempotency keys let clients retry safely by returning the original response instead of re-executing side effects.
- 2Hashing the request body detects a reused key with a mismatched payload, preventing accidental cross-request collisions.
- 3An atomic set-if-not-exists acts as a lock so concurrent duplicate requests can't both run the view.
Related explainers
python
from fastapi import FastAPI, WebSocket, WebSocketDisconnect app = FastAPI()
Building a WebSocket chat with FastAPI
websockets
broadcast
connection-management
Intermediate
9 steps
python
import time import uuid from django.utils.deprecation import MiddlewareMixin
Attaching per-request context in Django
middleware
request lifecycle
multi-tenancy
Intermediate
7 steps
javascript
import { useState, useEffect, useCallback, useRef } from 'react'; const cache = new Map(); const inflight = new Map();
Building a stale-while-revalidate hook in React
caching
request-deduplication
custom-hooks
Advanced
10 steps
go
func (w *Watcher) resetDebounce(d time.Duration) { if !w.timer.Stop() { select { case <-w.timer.C:
Debouncing a stream of events in Go
debounce
timers
channels
Advanced
7 steps
python
import random from typing import Iterator, List
How reservoir sampling picks k items
reservoir-sampling
streaming
randomness
Intermediate
5 steps
php
<?php namespace App\Services;
Building a cached daily leaderboard in Laravel
caching
aggregation
eager-loading
Intermediate
9 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/building-an-idempotency-decorator-in-flask-explained-python-b0b1/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.