L’idempotence est la propriété qui rend les retries automatiques sûrs. Une API idempotente peut être appelée 10 fois avec les mêmes paramètres, l’effet est le même qu’un seul appel. Sans cette propriété, un timeout réseau qui déclenche un retry crée deux commandes, deux paiements, deux envois d’email. Cette page pose la définition formelle, identifie les verbes HTTP idempotents et non idempotents, présente le pattern idempotency key rendu populaire par Stripe, explique pourquoi le cloud impose cette discipline et défend une opinion : l’idempotence n’est pas une option, c’est un prérequis de toute API exposée à des retries automatiques — c’est-à-dire toute API dans le cloud.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- La définition mathématique de l’idempotence et son enjeu cloud
- Les verbes HTTP idempotents (GET, PUT, DELETE) vs non idempotents (POST)
- Le pattern idempotency key popularisé par Stripe
- Les 3 patterns d’implémentation : token, conditional update, dedup
- Pourquoi le cloud impose l’idempotence par construction
Prérequis : avoir compris loose coupling et synchrone vs asynchrone. Si besoin, lisez d’abord Loose coupling et stateless design et Synchrone vs asynchrone.
1. Définition formelle et conséquence pratique
Section intitulée « 1. Définition formelle et conséquence pratique »Définition mathématique
Section intitulée « Définition mathématique »Une opération f est idempotente si l’appliquer plusieurs fois équivaut à l’appliquer une seule fois :
f(f(x)) == f(x)Et plus généralement, pour tout n ≥ 1 :
f^n(x) == f(x)Mathématiquement simple. Opérationnellement, c’est une discipline subtile à mettre en œuvre.
Pourquoi le cloud l’impose
Section intitulée « Pourquoi le cloud l’impose »Trois mécaniques propres au cloud rendent l’idempotence obligatoire sur les API et workflows.
Mécanique 1 — Les retries automatiques. Quand un appel HTTP timeout (lié, par exemple, à une latence réseau ponctuelle), le client retry par défaut. AWS SDK, Azure SDK, GCP SDK, Outscale SDK font tous des retries automatiques. Si votre API n’est pas idempotente, un timeout retry crée deux ressources.
Mécanique 2 — Les message queues at-least-once. La majorité des queues (SQS, Service Bus, Pub/Sub) garantissent une livraison au moins une fois. En cas de doute, le message est redélivré. Votre consommateur doit donc traiter le même message plusieurs fois sans effet de bord.
Mécanique 3 — Les workflows distribués. Step Functions, Durable Functions et autres orchestrateurs rejouent une étape en cas d’échec ou de redémarrage de l’orchestrateur. Sans idempotence, le rejeu crée des doublons.
2. Verbes HTTP — qui est idempotent ?
Section intitulée « 2. Verbes HTTP — qui est idempotent ? »La RFC 7231 (HTTP/1.1) classifie les verbes selon trois propriétés : safe (lecture seule), idempotent, et cacheable. Voici le tableau.
| Verbe | Safe | Idempotent | Usage typique |
|---|---|---|---|
| GET | ✅ | ✅ | Lecture (pas d’effet de bord) |
| HEAD | ✅ | ✅ | Métadonnées de lecture |
| OPTIONS | ✅ | ✅ | Capabilities du serveur |
| PUT | ❌ | ✅ | Création ou remplacement complet |
| DELETE | ❌ | ✅ | Suppression |
| PATCH | ❌ | ⚠️ (selon implémentation) | Modification partielle |
| POST | ❌ | ❌ | Création (ou action quelconque) |
Pourquoi PUT est idempotent et POST ne l’est pas
Section intitulée « Pourquoi PUT est idempotent et POST ne l’est pas »PUT modifie une ressource à un identifiant connu. PUT /users/42 avec un payload remplace l’utilisateur 42. Le faire 10 fois donne le même résultat — l’utilisateur 42 a la valeur du dernier PUT.
POST crée une ressource avec un identifiant attribué par le serveur. POST /orders avec un payload crée une commande, le serveur retourne l’ID 12345. Le faire 10 fois crée 10 commandes différentes (12345, 12346, 12347…). Pas idempotent.
PATCH — le cas ambigu
Section intitulée « PATCH — le cas ambigu »PATCH peut être idempotent ou non selon le format de patch utilisé.
PATCH idempotent : PATCH /users/42 avec body {"email": "new@example.com"} (Merge Patch RFC 7396). Remplace le champ email à la valeur fixée. Dix fois → même résultat.
PATCH non idempotent : PATCH /counters/42 avec body {"increment": 1} (custom). Incrémente le compteur de 1. Dix fois → +10. Pas idempotent.
La discipline est : si vous écrivez une API PATCH, forcer la sémantique idempotente par convention d’équipe.
3. Pattern idempotency key — la discipline POST
Section intitulée « 3. Pattern idempotency key — la discipline POST »Pour rendre POST idempotent, le pattern dominant en 2026 est l’idempotency key, popularisé par Stripe. Le client génère un identifiant unique par opération métier (UUID typiquement) et l’envoie dans un en-tête HTTP.
Le mécanisme
Section intitulée « Le mécanisme »POST /v1/charges HTTP/1.1Host: api.stripe.comIdempotency-Key: a3b2c1d4-7890-4def-9876-1234567890abContent-Type: application/json
{ "amount": 2000, "currency": "eur", "source": "tok_visa"}Côté serveur :
- Recevoir la requête, extraire l’
Idempotency-Key. - Vérifier en base si cette clé existe déjà.
- Si oui : retourner la réponse précédemment calculée, sans re-exécuter.
- Si non : exécuter, stocker la réponse associée à la clé, retourner la réponse.
Avec ce pattern, le client peut retry indéfiniment avec la même idempotency-key, le serveur ne créera la charge qu’une fois.
Implémentation côté serveur — DynamoDB
Section intitulée « Implémentation côté serveur — DynamoDB »Pour une stack AWS, DynamoDB convient parfaitement avec sa fonctionnalité de conditional write.
import boto3from botocore.exceptions import ClientError
table = boto3.resource('dynamodb').Table('idempotency_keys')
def process_request(idempotency_key, payload): # Tentative d'insertion avec condition unique try: table.put_item( Item={ 'key': idempotency_key, 'status': 'processing', 'request': payload, }, ConditionExpression='attribute_not_exists(#k)', ExpressionAttributeNames={'#k': 'key'} ) except ClientError as e: if e.response['Error']['Code'] == 'ConditionalCheckFailedException': # Clé déjà existante, retourner la réponse précédente existing = table.get_item(Key={'key': idempotency_key}) if existing['Item']['status'] == 'completed': return existing['Item']['response'] else: # Encore en cours de traitement par un autre worker return {'status': 409, 'message': 'In progress'} raise
# Premier traitement : exécuter response = execute_business_logic(payload) table.update_item( Key={'key': idempotency_key}, UpdateExpression='SET #s = :s, #r = :r', ExpressionAttributeNames={'#s': 'status', '#r': 'response'}, ExpressionAttributeValues={':s': 'completed', ':r': response} ) return responseDurée de rétention de la clé
Section intitulée « Durée de rétention de la clé »L’idempotency-key doit être conservée suffisamment longtemps pour absorber les retries — typiquement 24 heures. Au-delà, on peut purger pour libérer du stockage. Stripe garde 24 heures, AWS API Gateway propose 2 minutes à 60 minutes selon la config.
Pour des opérations très critiques (paiements importants), 7 jours peuvent être justifiés.
4. Trois patterns d’implémentation alternatifs
Section intitulée « 4. Trois patterns d’implémentation alternatifs »Pattern 1 — Token de transaction unique
Section intitulée « Pattern 1 — Token de transaction unique »Le client demande un token unique au serveur avant l’opération principale.
1. Client → POST /api/transactions/init → Server retourne token=abc1232. Client → POST /api/orders + token=abc123 → Server crée la commande, marque le token utilisé3. Si retry avec token=abc123 → Server détecte token utilisé, retourne réponse précédenteTrès adapté aux formulaires web : le serveur génère un token au chargement de la page, le formulaire l’inclut comme champ caché. Le double-submit du formulaire est neutralisé.
Pattern 2 — Conditional update (lock optimiste)
Section intitulée « Pattern 2 — Conditional update (lock optimiste) »Au lieu d’une clé d’idempotence externe, on utilise un numéro de version sur la ressource.
PUT /users/42 HTTP/1.1If-Match: "v17"
{ "email": "new@example.com" }Le serveur accepte la modification uniquement si la version actuelle est v17. Si un autre client a déjà modifié à v18, la requête échoue avec un 412 Precondition Failed. Le client peut alors choisir de re-lire et de re-tenter.
Utile pour les modifications concurrentes où l’idempotence par clé ne suffit pas — on ajoute la garantie qu’on modifie bien la version qu’on a lue.
Pattern 3 — Deduplication par contenu
Section intitulée « Pattern 3 — Deduplication par contenu »Pour des messages identiques à dédupliquer (ex. : webhooks, événements), on calcule un hash du contenu et on vérifie l’unicité.
import hashlibimport json
def dedupe_message(message): content_hash = hashlib.sha256( json.dumps(message, sort_keys=True).encode() ).hexdigest()
if redis_client.set(f'msg:{content_hash}', '1', ex=3600, nx=True): # Première fois qu'on voit ce hash : traiter process_message(message) else: # Déjà vu dans la dernière heure : ignorer log.info(f'Duplicate message ignored: {content_hash}')Adapté aux flux d’événements où les producteurs ne peuvent pas fournir d’idempotency-key explicite (webhooks externes par exemple).
5. At-most-once, at-least-once, exactly-once
Section intitulée « 5. At-most-once, at-least-once, exactly-once »Les trois sémantiques de delivery messaging
Section intitulée « Les trois sémantiques de delivery messaging »Quand un message est publié dans une queue ou un bus, trois sémantiques de livraison sont possibles.
| Sémantique | Garantie | Risque |
|---|---|---|
| At-most-once | Le message est livré 0 ou 1 fois | Perte possible |
| At-least-once | Le message est livré 1 fois ou plus | Doublons possibles |
| Exactly-once | Le message est livré exactement 1 fois | Théorique, complexe |
La réalité du « exactly-once »
Section intitulée « La réalité du « exactly-once » »L’exactly-once est techniquement impossible sur un système distribué dans le cas général (théorème FLP). Les solutions qui annoncent exactly-once (Kafka avec EOS, Pulsar) le réalisent dans des conditions très contraintes : producteur idempotent, transactions Kafka, consommateur idempotent. Ce n’est pas magique — c’est de la combinaison.
La discipline pratique : accepter que vous êtes en at-least-once, et rendre le traitement idempotent côté consommateur. C’est la seule approche qui marche en pratique sur SQS, Service Bus, Pub/Sub.
Conséquence sur l’architecture
Section intitulée « Conséquence sur l’architecture »Toute application cloud qui consomme des messages doit être idempotente, parce que la queue est at-least-once. Cette contrainte est structurelle, pas optionnelle.
def handle_sqs_message(message): business_id = message['body']['transaction_id']
# Vérifier si déjà traité if dynamodb.get_item(Key={'transaction_id': business_id}).get('Item'): # Déjà traité, supprimer le message de la queue et sortir sqs.delete_message(QueueUrl=queue_url, ReceiptHandle=message['ReceiptHandle']) return
# Traiter et marquer comme traité (idempotence) process_transaction(message['body']) dynamodb.put_item(Item={'transaction_id': business_id, 'processed_at': now()})
sqs.delete_message(QueueUrl=queue_url, ReceiptHandle=message['ReceiptHandle'])6. Idempotence et workflows distribués
Section intitulée « 6. Idempotence et workflows distribués »Les orchestrateurs (AWS Step Functions, Azure Durable Functions, GCP Workflows) rejouent les étapes en cas d’échec. Chaque task d’un workflow doit être idempotente.
Pattern Step Functions avec idempotency
Section intitulée « Pattern Step Functions avec idempotency »StartAt: ProcessOrderStates: ProcessOrder: Type: Task Resource: arn:aws:lambda:eu-west-3:...:function:CreateOrder Parameters: orderId.$: $.orderId # Identifiant métier stable idempotencyKey.$: $$.Execution.Name # Identifiant Step Functions Retry: - ErrorEquals: [States.ALL] IntervalSeconds: 2 MaxAttempts: 3 BackoffRate: 2.0 Next: ChargePaymentL’identifiant $$.Execution.Name est stable pour toute la durée du workflow. Si Step Functions retry l’étape ProcessOrder, le Lambda reçoit la même idempotency-key. Le Lambda peut détecter la duplication et retourner la réponse précédente.
Le piège du « presque idempotent »
Section intitulée « Le piège du « presque idempotent » »Un Lambda qui exécute « si la commande n’existe pas, la créer » semble idempotent. Mais entre le SELECT et le INSERT, une fenêtre de race condition existe. Deux retries simultanés peuvent passer le SELECT en même temps et faire deux INSERT.
Solution : utiliser une contrainte d’unicité en base (UNIQUE INDEX) qui rejette le second INSERT, ou utiliser un INSERT ... ON CONFLICT DO NOTHING (PostgreSQL), ou un conditional write DynamoDB.
7. Bonnes pratiques pour tenir l’idempotence dans la durée
Section intitulée « 7. Bonnes pratiques pour tenir l’idempotence dans la durée »L’idempotence ne se décrète pas une fois pour toutes : elle se conçoit en amont, se teste régulièrement, et se vérifie en continu au fil des évolutions du code. Quelques bonnes pratiques permettent d’éviter les pièges classiques.
Implémenter l’idempotence dès la conception. Une API qui accepte des opérations modifiantes (POST en HTTP, écritures dans une queue) sans mécanisme d’idempotence finit par produire des doublons en production. Ce coût se manifeste sous forme d’incidents de duplication : double facturation, double envoi de notification, double création d’entité. Le coût d’implémentation initial — quelques jours de travail par service — reste largement inférieur au coût d’un incident en production, qu’il faut souvent traiter manuellement. Sur les opérations financières et les paiements, l’idempotence est considérée comme non négociable.
Comprendre la limite de l’exactly-once. Plusieurs systèmes de messaging annoncent une garantie exactly-once. Cette garantie est réelle, mais sous conditions : producteur idempotent configuré, consommateur idempotent, transactions activées de bout en bout. En pratique, la combinaison at-least-once + idempotence côté consommateur reste plus simple à raisonner et à tester, et couvre les mêmes cas d’usage avec moins de configuration sensible.
Tester l’idempotence activement. Une API idempotente lors de sa mise en production peut cesser de l’être plus tard si un développeur ajoute un effet de bord sans en mesurer l’impact. Trois pratiques permettent de prévenir cette dérive :
- Tests automatisés qui appellent l’API plusieurs fois avec la même clé et vérifient que l’état final est identique.
- Tests de chaos périodiques qui rejouent massivement des messages de queue en environnement de pré-production.
- Post-mortem systématique sur tout incident lié à des doublons, pour identifier la cause racine et corriger.
Documenter le contrat d’idempotence. Indiquer clairement, dans la documentation de chaque API ou consommateur de queue, le comportement attendu en cas de retry : quelle clé identifier, combien de temps elle est conservée, quel code de retour signale une duplication. Cette documentation est ce qui permet aux clients de s’aligner correctement sur le contrat.
À retenir
Section intitulée « À retenir »- Idempotence :
f(f(x)) == f(x). Appliquer 10 fois la même opération équivaut à l’appliquer 1 fois. - Le cloud impose l’idempotence : retries automatiques, queues at-least-once, workflows distribués qui rejouent.
- Verbes HTTP idempotents : GET, PUT, DELETE. Non idempotent : POST. PATCH selon implémentation.
- Idempotency key (pattern Stripe) rend POST idempotent : client génère UUID, serveur stocke et déduplique.
- Trois patterns d’implémentation : token de transaction, conditional update (version), deduplication par hash.
- Exactly-once est un mirage — accepter at-least-once + idempotence consommateur est la seule approche fiable.
- L’idempotence se teste activement — sans tests automatisés, elle dérive avec les évolutions du code.