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'avec 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. Ces deux notions précèdent cette leçon dans la formation : Loose coupling et stateless design et Synchrone vs asynchrone. Cette page traite l'idempotence d'une API ou d'un consommateur de messages. L'idempotence d'un outil de configuration (rejouer un playbook ou un terraform apply sans rien changer) est le même principe appliqué à l'infrastructure, présenté dans Déclaratif vs impératif.
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 dépasse son délai (une latence réseau ponctuelle suffit), le client le rejoue (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 9110 (HTTP Semantics, qui a remplacé la RFC 7231) définit les méthodes safe (lecture seule, section 9.2.1) et idempotentes (section 9.2.2), et précise lesquelles se mettent en cache (section 9.2.3). Voici le tableau.
| Verbe | Safe | Idempotent | Usage typique |
|---|---|---|---|
| GET | Oui | Oui | Lecture (pas d'effet de bord) |
| HEAD | Oui | Oui | Métadonnées de lecture |
| OPTIONS | Oui | Oui | Capabilities du serveur |
| PUT | Non | Oui | Création ou remplacement complet |
| DELETE | Non | Oui | Suppression |
| PATCH | Non | Partiel (selon implémentation) | Modification partielle |
| POST | Non | Non | 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.
Précision utile : l'idempotence porte sur l'état final du serveur, pas sur la réponse. Un second DELETE /users/42 peut renvoyer 404 au lieu de 204, mais l'état reste le même : la ressource n'existe plus.
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 le plus courant est l'idempotency key, popularisé par Stripe. Le client génère un identifiant unique par opération métier (typiquement un UUID, identifiant aléatoire pratiquement impossible à rencontrer deux fois) et l'envoie dans un en-tête HTTP.
Le mécanisme
Section intitulée « Le mécanisme »Le client choisit une clé unique avant le premier envoi et la réutilise telle quelle à chaque nouvel essai de la même opération. Voici la requête, dans l'en-tête Idempotency-Key que l'API de Stripe attend.
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.
Le schéma suivant rejoue l'erreur classique de timeout de la section précédente. Regardez la flèche barrée : la réponse du premier appel est perdue, alors que le paiement a bien eu lieu. Au retry, le serveur reconnaît la clé K dans sa table et renvoie la réponse stockée au lieu de rejouer le paiement.
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 responseCe code a une faille à connaître : si execute_business_logic échoue, la clé reste à l'état processing et les retries reçoivent indéfiniment un 409. Deux parades : attraper l'échec pour passer la clé en statut failed (ou la supprimer), ce qui autorise une nouvelle tentative, et poser une expiration (TTL DynamoDB) sur l'état processing. Cette expiration doit être supérieure à la durée maximale du traitement : si elle tombe pendant qu'un worker travaille encore, un retry reprend la clé et exécute l'opération une seconde fois, ce qui recrée le doublon que la clé devait éviter.
Duré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 (API v1) peut purger les clés après au moins 24 heures, et l'utilitaire d'idempotence d'AWS Lambda Powertools expire ses enregistrements au bout de 1 heure par défaut (réglable avec expires_after_seconds). Amazon API Gateway n'offre pas, à notre connaissance, de fonction native de clé d'idempotence : c'est à l'application de la gérer.
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}')Ce pattern a des limites sérieuses. Deux messages légitimement identiques (deux achats identiques du même client) sont pris pour un doublon, et le second est perdu. La fenêtre de 1 heure laisse repasser un vrai doublon arrivé après la fenêtre. Et si process_message échoue après l'écriture dans Redis, le message est marqué « vu » alors qu'il n'a pas été traité : il est perdu au retry. Réservez-le aux flux sans identifiant exploitable, et préférez dès que possible un identifiant de livraison fourni par l'émetteur : les webhooks en portent un (identifiant d'événement Stripe, en-tête X-GitHub-Delivery de GitHub), à utiliser comme clé d'idempotence.
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 (la livraison exactement une fois sur un lien non fiable se heurte au problème des deux généraux ; le théorème FLP, lui, établit qu'aucun algorithme de consensus déterministe ne peut garantir de terminer dans un système asynchrone si un seul processus peut tomber en panne). 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.
# Pseudo-code : le client DynamoDB et les fonctions métier sont supposés définisdef handle_sqs_message(message): business_id = message['body']['transaction_id']
# Réserver l'identifiant de façon atomique : un seul worker réussit try: dynamodb.put_item( Item={'transaction_id': business_id, 'status': 'processing'}, ConditionExpression='attribute_not_exists(transaction_id)', ) except ConditionalCheckFailedException: item = dynamodb.get_item(Key={'transaction_id': business_id}).get('Item', {}) if item.get('status') == 'completed': # Déjà traité : acquitter le doublon, sans retraiter sqs.delete_message(QueueUrl=queue_url, ReceiptHandle=message['ReceiptHandle']) # Sinon un autre worker traite, ou a échoué sans libérer sa réservation : # ne PAS acquitter. La file relivrera le message après son délai de # visibilité, et l'expiration de la réservation (section 3) débloque le # cas du worker mort. return
try: process_transaction(message['body']) except Exception: # Libérer la réservation pour qu'un retry puisse repasser dynamodb.delete_item(Key={'transaction_id': business_id}) raise
dynamodb.update_item( Key={'transaction_id': business_id}, UpdateExpression='SET #s = :done', ExpressionAttributeNames={'#s': 'status'}, ExpressionAttributeValues={':done': 'completed'}, ) sqs.delete_message(QueueUrl=queue_url, ReceiptHandle=message['ReceiptHandle'])Ce schéma réserve l'identifiant avant de traiter, avec une écriture conditionnelle atomique : deux workers qui reçoivent le même message en même temps ne peuvent pas tous deux passer. Lire d'abord (get_item), traiter, puis écrire (put_item) laisserait au contraire une fenêtre de race condition, exactement celle que décrit la section « Le piège du presque idempotent » plus bas. Le traitement lui-même doit rester sûr à rejouer si le worker meurt entre la réservation et la fin : c'est la limite de tout schéma sans transaction englobante.
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 ensuite 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 n'existe que sous conditions strictes : accepter at-least-once + idempotence côté consommateur est l'approche la plus fiable.
- L'idempotence se teste activement, sans tests automatisés, elle dérive avec les évolutions du code.
Contrôle de connaissances
Section intitulée « Contrôle de connaissances »Six questions tirées sur huit : ce qu'est l'idempotence, les méthodes HTTP concernées, la clé d'idempotence et les reprises sans doublon.
Contrôle de connaissances
Validez vos connaissances avec ce quiz interactif
Informations
- Le chronomètre démarre au clic sur Démarrer
- Questions à choix multiples, vrai/faux et réponses courtes
- Vous pouvez naviguer entre les questions
- Les résultats détaillés sont affichés à la fin
Lance le quiz et démarre le chronomètre
Vérification
(0/0)Score par domaine
Un domaine regroupe plusieurs guides et reçoit plusieurs questions : son score est plus fiable qu'un score par thème, qui porte souvent sur une seule question.
Bilan
Points forts
Aucun sujet réussi à 80 % sur au moins deux questions pour l’instant.
Points faibles
Aucun sujet sous le seuil de réussite.
À faire ensuite
Des indices pour retenter votre chance ?
Continuer le cours : Élasticité vs scalabilité cloud : distinguer les 2 concepts confondus
Nouveau quiz complet avec des questions aléatoires
Retravailler uniquement les questions ratées
Retour à la liste des certifications