Aller au contenu
Cloud high

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

16 min de lecture

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.

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

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

La RFC 7231 (HTTP/1.1) classifie les verbes selon trois propriétés : safe (lecture seule), idempotent, et cacheable. Voici le tableau.

VerbeSafeIdempotentUsage typique
GETLecture (pas d’effet de bord)
HEADMétadonnées de lecture
OPTIONSCapabilities du serveur
PUTCréation ou remplacement complet
DELETESuppression
PATCH⚠️ (selon implémentation)Modification partielle
POSTCré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 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 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.

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.

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

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.

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}')

Adapté aux flux d’événements où les producteurs ne peuvent pas fournir d’idempotency-key explicite (webhooks externes par exemple).

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

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'])

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

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

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn