Aller au contenu
English
Compte : non connecté, synchronisation possible Mon apprentissage
Selectionner le thème
English

Idempotence cloud : la propriété critique des API et workflows

Par mis à jour le

Améliorer cette page

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.

  • 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.

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.

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.

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.

VerbeSafeIdempotentUsage typique
GETOuiOuiLecture (pas d'effet de bord)
HEADOuiOuiMétadonnées de lecture
OPTIONSOuiOuiCapabilities du serveur
PUTNonOuiCréation ou remplacement complet
DELETENonOuiSuppression
PATCHNonPartiel (selon implémentation)Modification partielle
POSTNonNonCréation (ou action quelconque)

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 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.

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 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.1
Host: api.stripe.com
Idempotency-Key: a3b2c1d4-7890-4def-9876-1234567890ab
Content-Type: application/json
{
"amount": 2000,
"currency": "eur",
"source": "tok_visa"
}

Côté serveur :

  1. Recevoir la requête, extraire l'Idempotency-Key.
  2. 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.

Diagramme de séquence : le client envoie un POST avec une Idempotency-Key K, le serveur réserve K, exécute le paiement, stocke la réponse, mais la réponse 200 est perdue. Le client rejoue le même POST avec la même clé K, le serveur trouve K dans sa table et renvoie la réponse stockée, sans second paiement.Table des clésServeurClientPOST /v1/charges +Idempotency-Key KK existe ? Non,réserver KExécuter lepaiementStocker la réponsepour KRéponse 200 perdue(timeout)Retry : même POST,même clé KK existe ? OuiRéponse stockéeMême réponse, aucunsecond paiement
Source · SVG
Échap pour fermer, + et − pour zoomer, défilement pour se déplacer. 100 %

Pour une stack AWS, DynamoDB convient parfaitement avec sa fonctionnalité de conditional write.

import boto3
from 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 response

Ce 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.

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.

Le client demande un token unique au serveur avant l'opération principale.

1. Client → POST /api/transactions/init → Server retourne token=abc123
2. 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édente

Trè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é.

Au lieu d'une clé d'idempotence externe, on utilise un numéro de version sur la ressource.

PUT /users/42 HTTP/1.1
If-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.

Pour des messages identiques à dédupliquer (ex. : webhooks, événements), on calcule un hash du contenu et on vérifie l'unicité.

import hashlib
import 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.

Quand un message est publié dans une queue ou un bus, trois sémantiques de livraison sont possibles.

SémantiqueGarantieRisque
At-most-onceLe message est livré 0 ou 1 foisPerte possible
At-least-onceLe message est livré 1 fois ou plusDoublons possibles
Exactly-onceLe message est livré exactement 1 foisThéorique, complexe

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.

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éfinis
def 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.

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.

StartAt: ProcessOrder
States:
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: ChargePayment

L'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.

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 :

  1. Tests automatisés qui appellent l'API plusieurs fois avec la même clé et vérifient que l'état final est identique.
  2. Tests de chaos périodiques qui rejouent massivement des messages de queue en environnement de pré-production.
  3. 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.

  • 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.

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

6 questions
6 min.
70% requis

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

Ce projet vous est utile ?

Les guides, formations, quiz et labs restent gratuits. Si vous les utilisez régulièrement, vous pouvez contribuer à leur maintenance et à leur développement.

Devenir membre de soutien (nouvel onglet) Soutenir une fois (nouvel onglet) En savoir plus

Suivez aussi le projet sur LinkedIn