L'Idempotence dans les Systèmes Distribués Qui Fonctionne Vraiment
Éviter les effets de bord dupliqués
L’idempotence dans les systèmes distribués est la propriété qui vous sauve lorsque le réseau ment, la file d’attente rejoue, le client panique et l’opérateur effectue un rejou. Dans les systèmes de production, la livraison en double est la norme. Les effets de bord en double constituent le bug.
Le protocole HTTP définit une méthode idempotente comme une méthode où plusieurs identiques requêtes ont le même effet prévu sur le serveur qu’une seule requête. C’est pourquoi les méthodes PUT, DELETE et les méthodes sûres sont idempotentes selon la sémantique du protocole et peuvent être réessayées automatiquement après une défaillance de communication.

Cette définition est utile, mais elle est insuffisante. Dans les architectures réelles, l’idempotence n’est pas une réponse de trivia HTTP. C’est une garantie métier. Si un client clique sur « payer » une fois, vous ne pouvez pas facturer deux fois parce qu’un délai d’expiration s’est produit entre l’engagement et la réponse. Si un travailleur met à jour l’inventaire et plante avant d’accuser réception du message, vous ne pouvez pas décrémenter le stock deux fois parce que le courtier a redistribué le message. C’est le standard.
L’erreur que je vois encore et encore est de traiter l’idempotence comme une fonctionnalité de transport plutôt que comme une propriété du système. La déduplication de file d’attente, les verbes HTTP et les tentatives de reconnexion du client aident, mais aucun d’eux ne sauve une conception qui permet à la même intention commerciale de créer un second effet de bord. Si vous souhaitez une vue d’ensemble plus large sur la façon dont ces décisions d’intégration s’inscrivent dans les limites des services et les compromis de persistance, commencez par Architecture d’application en production : modèles d’intégration, conception de code et accès aux données.
D’où viennent les doublons en production
Les doublons n’apparaissent pas parce que les équipes sont négligentes. Ils apparaissent parce que les systèmes distribués rejouent, réordonnent et rejouent.
Un client peut envoyer une requête de création, le serveur peut l’engager, et la réponse peut quand même disparaître sur le fil. C’est exactement pourquoi HTTP distingue les méthodes idempotentes et pourquoi les API de paiement telles que Stripe et PayPal exposent des mécanismes d’idempotence explicites pour les méthodes non sûres comme POST.
Les courtiers de messages rendent le problème encore plus évident. La livraison au moins une fois signifie qu’un consommateur peut être invoqué à plusieurs reprises pour le même message, et un gestionnaire peut mettre à jour la base de données avec succès mais échouer avant l’accusé de réception, ce qui amène le courtier à redistribuer le même message.
Les webhooks ne sont pas différents. GitHub indique que les livraisons de webhooks peuvent arriver dans le désordre, les livraisons échouées ne sont pas automatiquement redistribuées, et chaque livraison contient un GUID X-GitHub-Delivery unique que vous devez utiliser pour vous protéger contre le rejou. Pour une vue d’architecture pratique des points de terminaison de chat comme limites d’interaction, voir Les plateformes de chat comme interfaces système dans les systèmes modernes.
Même les systèmes qui annoncent des garanties plus fortes vous laissent encore du travail à faire. Kafka peut empêcher les entrées en double dans les journaux Kafka avec des producteurs idempotents et peut fournir une livraison exactement une fois pour les flux de lecture-traitement-écriture qui restent dans Kafka avec des transactions et des consommateurs read_committed. Mais la documentation de conception de Kafka est claire sur le fait que les systèmes externes nécessitent toujours une coordination avec les décalages et les sorties. La livraison exactement une fois de Google Cloud Pub/Sub est limitée aux abonnements de traction, au sein d’une région cloud, et nécessite toujours que les clients suivent la progression du traitement jusqu’à ce que l’accusé de réception réussisse.
Mon résumé d’opinion est simple. Supposez que le transport rejouera. Supposez que les opérateurs rejoueront. Supposez que les webhooks arriveront en retard. Concevez le chemin d’écriture de manière à ce qu’une intention répétée ne puisse pas créer un second effet métier.
Le contrat API auquel je fais réellement confiance
Comment les clés d’idempotence empêchent les requêtes API en double
Le seul contrat API auquel je fais confiance pour les opérations de mutation est l’intention fournie par l’appelant plus la persistance côté serveur.
AWS recommande un identifiant de requête fourni par l’appelant et avertit que le service doit enregistrer atomiquement le jeton d’idempotence avec le travail de mutation. Stripe stocke le premier code d’état et le corps de la réponse pour une clé, compare les paramètres ultérieurs avec la requête d’origine et renvoie le même résultat pour les tentatives de reconnexion. PayPal utilise PayPal-Request-Id sur les API POST prises en charge et renvoie le dernier statut pour la requête précédente avec le même en-tête.
Cela conduit à un contrat pratique :
- Le client génère une clé d’idempotence pour une opération commerciale.
- Le serveur étend cette clé par locataire et nom d’opération.
- Le serveur stocke un hachage de requête pour que la même clé ne puisse pas être réutilisée pour une charge utile différente.
- Le serveur enregistre l’état tel que
en attente,terminéouéchoué. - Les tentatives de reconnexion avec la même clé renvoient soit le résultat stocké, soit un pointeur stable vers celui-ci.
- Les tentatives de reconnexion avec la même clé et une charge utile différente échouent bruyamment.
Il existe un brouillon d’en-tête Idempotency-Key de l’IETF, mais au 09/05/2026, il est toujours répertorié dans le Datatracker de l’IETF comme un Internet-Draft expiré plutôt qu’une RFC publiée. En pratique, le nom de l’en-tête est toujours largement utile comme convention de facto, mais vous devriez documenter le contrat dans votre propre API plutôt que de prétendre que la norme est terminée.
Que doit représenter la clé ? L’intention. Pas une tentative HTTP. Pas une connexion TCP. Pas un compteur de reconnexion. Si l’utilisateur signifie « créer la commande 123 une fois », chaque tentative de reconnexion pour cette même commande doit réutiliser la même clé. Si l’utilisateur signifie « passer une deuxième commande », cela doit utiliser une clé différente.
Un ID de requête est pour le traçage. Une clé d’idempotence est pour la correction. Si vous les mélangez, vos tableaux de bord semblent propres tandis que votre argent bouge deux fois.
Pourquoi PUT n’est pas suffisant
Non, HTTP PUT n’est pas suffisant pour rendre une opération idempotente.
Oui, la RFC 9110 donne à PUT des sémantiques idempotentes. Mais si votre gestionnaire PUT émet un nouvel événement en aval, envoie un e-mail à chaque tentative de reconnexion ou facture à nouveau un fournisseur externe, alors votre implémentation a violé le contrat commercial même si le nom de votre route semble respectable.
Le choix du verbe aide les clients à comprendre l’intention. Il n’implémente pas l’intention pour vous.
Utilisez PUT lorsque le modèle de ressource correspond vraiment à une opération de remplacement complet ou de type upsert. Utilisez POST lorsque vous créez des commandes ou des actions. Mais pour toute mutation qui pourrait être rejouée à travers les limites du réseau, documentez un contrat d’idempotence explicite. Si vos actions de mutation sont déclenchées à partir de workflows de chat, le même contrat s’applique dans Modèles d’intégration Slack pour les alertes et les workflows et Modèle d’intégration Discord pour les alertes et les boucles de contrôle. Les effets de bord cachés sont là où l’architecture va mourir.
Combien de temps une clé d’idempotence doit-elle être stockée
Plus longtemps que votre équipe de transport ne le souhaite.
Stripe indique que les clés peuvent être élaguées après au moins 24 heures. PayPal indique que la rétention est spécifique à l’API et donne des exemples qui peuvent durer jusqu’à 45 jours. Amazon SQS FIFO déduplique uniquement dans une fenêtre de 5 minutes. GitHub conserve les livraisons récentes pendant 3 jours pour la redistribution manuelle. Ces chiffres sont wildly différents parce que la période de rétention correcte est une décision commerciale, pas une valeur par défaut de protocole.
Si vous ne gardez les clés que cinq minutes parce que votre file d’attente le fait, vous ne concevez pas l’idempotence. Vous copiez une limitation de transport dans votre couche métier.
Conservez les enregistrements d’idempotence pendant au moins le maximum de ces fenêtres :
- horizon de reconnexion du client
- horizon de reconnexion de la file d’attente
- horizon de rejou des webhooks
- horizon de rejou de l’opérateur
- horizon de règlement ou de compensation pour les opérations de mouvement d’argent
Pour les paiements, les réservations et le provisionnement, cela signifie souvent des heures ou des jours, pas des minutes.
AWS signale également deux anti-modèles avec lesquels je suis entièrement d’accord. N’utilisez pas les horodatages comme clé, car le décalage d’horloge et les collisions les rendent peu fiables. Ne stockez pas aveuglément les charges utiles de requête entières comme enregistrement de déduplication pour chaque requête, car cela nuit aux performances et à l’évolutivité. Stockez un hachage de requête normalisé plus l’état de réponse minimum dont vous avez besoin pour rejouer en toute sécurité. Si vous devez reproduire la première réponse byte pour byte, stockez le corps de la réponse canonique comme le fait Stripe.
Les modèles de base de données qui rendent l’idempotence réelle
L’idempotence devient réelle lorsque la couche de persistance peut gagner une course exactement une fois.
PostgreSQL vous donne deux primitives critiques ici. Les contraintes uniques imposent l’unicité sur une ou plusieurs colonnes, et INSERT ... ON CONFLICT vous permet de définir une action alternative au lieu d’échouer en cas de violation d’unicité. PostgreSQL documente également que ON CONFLICT DO UPDATE garantit un résultat d’insertion ou de mise à jour atomique sous concurrence.
Cela signifie que votre couche d’idempotence devrait généralement commencer par une table comme celle-ci :
create table api_idempotency (
tenant_id text not null,
operation text not null,
idempotency_key text not null,
request_hash text not null,
state text not null,
status_code integer,
response_body jsonb,
resource_type text,
resource_id text,
created_at timestamptz not null default now(),
expires_at timestamptz not null,
primary key (tenant_id, operation, idempotency_key)
);
Et le flux de traitement devrait ressembler à ceci :
begin transaction
try insert (tenant_id, operation, idempotency_key, request_hash, state='pending')
on conflict do nothing
load row for (tenant_id, operation, idempotency_key) for update
if row.request_hash != incoming_request_hash
fail with conflict or validation error
if row.state = 'completed'
return stored response
if row.state = 'pending' and row was created by another live request
either wait briefly, or fail fast with a retryable response
perform local business mutation
store stable result in idempotency row
set state = 'completed'
commit
return result
La partie importante n’est pas la syntaxe. La partie importante est l’atomicité. L’enregistrement de la clé et l’exécution de la mutation doivent réussir ou échouer ensemble. AWS le dit explicitement pour l’idempotence API, et la même règle s’applique aux services basés sur SQL.
Ne faites pas une séquence naïve de vérification-puis-action comme « sélectionner la clé ; si manquante alors insérer commande ». Sous concurrence, deux requêtes peuvent passer la vérification et toutes les deux créer l’effet de bord. Une contrainte unique n’est pas optionnelle. C’est le mécanisme qui transforme votre architecture de folklore optimiste en quelque chose que vous pouvez prouver sous charge.
Voici la règle que j’utilise dans les revues. Si la décision de déduplication n’est pas protégée par la même limite transactionnelle que la mutation, vous n’avez pas d’idempotence. Vous avez de l’espoir.
Les messages, événements et webhooks ont besoin de leur propre frontière
Comment les consommateurs gèrent les événements et messages en double
Pour les consommateurs de messages, le modèle classique est toujours le bon. Enregistrez les identifiants de message traités dans la même transaction de base de données que la mise à jour métier. Chris Richardson décrit l’approche de la table PROCESSED_MESSAGES directement, en utilisant une clé primaire sur l’abonné et l’identifiant de message pour que les doublons échouent proprement et puissent être ignorés.
De nombreuses équipes appellent ce magasin explicite processed_messages une table de boîte de réception. L’étiquette importe moins que la règle. Le récepteur doit persister la preuve qu’il a déjà traité le message avant qu’une tentative de reconnexion ne puisse faire rien en toute sécurité.
Une forme minimale ressemble à ceci :
create table processed_messages (
subscriber_id text not null,
message_id text not null,
processed_at timestamptz not null default now(),
primary key (subscriber_id, message_id)
);
Et le flux du consommateur est aussi strict que le flux HTTP :
begin transaction
insert into processed_messages (subscriber_id, message_id)
values (?, ?)
on conflict do nothing
if no row inserted
rollback
ack and ignore duplicate
apply business mutation
commit
ack message
Ce modèle est ennuyeux. Bien. L’idempotence devrait être ennuyeuse.
C’est aussi généralement mieux que d’essayer de s’appuyer sur les termes marketing des courtiers. Le support exactement une fois de Kafka est excellent lorsque vous restez dans le modèle transactionnel de Kafka lui-même, mais la documentation de Kafka avertit toujours que les destinations externes ont besoin de coopération. SQS FIFO réduit les envois en double uniquement dans sa fenêtre de déduplication de 5 minutes. La livraison exactement une fois de Pub/Sub attend toujours que l’abonné suive la progression et évite le travail en double lorsque les accusés de réception échouent.
Exactement une fois est généralement une optimisation locale. Les effets de bord idempotents sont la garantie du système.
Associez la déduplication avec le modèle de boîte de sortie
Si votre service met à jour l’état local et publie également un événement, la consommation idempotente seule n’est pas suffisante. Vous avez également besoin d’un moyen sûr de sortir l’événement après l’engagement de la transaction locale.
C’est pourquoi le modèle de boîte de sortie transactionnelle est important. Chris Richardson décrit l’idée de base comme écrire l’événement dans une table de boîte de sortie dans la même transaction que la mise à jour métier, puis le publier asynchroniquement. Debezium indique que le modèle de boîte de sortie évite les incohérences entre l’état interne d’un service et les événements consommés par d’autres services. NServiceBus va plus loin et montre comment le traitement de la boîte de sortie déduplique les messages entrants et évite les enregistrements zombies et les messages fantômes.
Voici l’architecture que je recommande pour les services qui possèdent des données et publient des événements d’intégration :
- Validez et persistez la commande sous une clé d’idempotence.
- Écrivez l’état métier et l’événement de boîte de sortie dans une seule transaction locale.
- Laissez CDC ou un expéditeur de boîte de sortie publier l’événement.
- Rendez les consommateurs en aval également idempotents.
La boîte de sortie ne supprime pas le besoin de consommateurs idempotents. Elle supprime le besoin de prétendre qu’un engagement de base de données et une publication de courtier peuvent être une seule transaction distribuée magique quand ils ne le peuvent généralement pas.
Les webhooks sont juste des messages avec un meilleur branding
Traitez les webhooks entrants exactement comme des messages d’un bord de réseau non fiable.
GitHub documente que les livraisons peuvent arriver dans le désordre, recommande d’utiliser X-Hub-Signature-256 pour vérifier l’authenticité, et fournit X-GitHub-Delivery comme identifiant de livraison unique. Il note également que les redistributions réutilisent le même ID de livraison.
Donc l’architecture est simple :
- vérifier la signature en premier
- utiliser le GUID de livraison comme clé de déduplication
- persister la réception avant les effets de bord
- rendre les gestionnaires sensibles à l’ordre plutôt que de supposer l’ordre d’arrivée
- mettre en file d’attente le travail lourd et retourner rapidement
Si votre gestionnaire de webhook écrit directement dans les tables métier avant d’enregistrer la réception, il n’est pas prêt pour la production. Il fait juste plus vite des erreurs en double.
Les sagas et moteurs de workflow ont toujours besoin d’idempotence
Les sagas et moteurs de workflow durables ne suppriment pas le problème. Ils le rendent visible.
Temporal recommande d’écrire des Activités pour être idempotentes parce que les Activités peuvent être rejouées après des échecs ou des délais d’expiration. Sa documentation signale même le cas limite où un travailleur complète un effet de bord externe avec succès mais plante avant de signaler la complétion, ce qui provoque la réexécution de l’Activité. Temporal suggère également d’utiliser une combinaison de l’ID d’exécution de Workflow et de l’ID d’Activité comme clé d’idempotence stable lors de l’appel de services en aval. Si vous appliquez cela dans l’orchestration de services, Microservices Go pour l’orchestration AI/ML couvre les compromis de workflow plus larges.
C’est exactement le bon modèle mental. Un moteur de workflow peut préserver l’historique d’exécution et coordonner les tentatives de reconnexion. Il ne peut pas rétroactivement annuler la facturation d’une carte ou annuler l’envoi d’un e-mail à moins que votre application lui donne des étapes idempotentes et des compensations idempotentes.
La même chose s’applique aux sagas. Les propres conseils de saga de Temporal décrivent des actions compensatrices qui s’exécutent lorsqu’une étape échoue. Ces compensations doivent également être idempotentes. Si « rembourser le paiement » s’exécute deux fois, vous pouvez avoir résolu le bug d’origine en créant un nouveau.
Ma règle ici est brutale et simple. Chaque Activité, chaque gestionnaire de commande, et chaque compensation qui touche le monde extérieur devrait soit être naturellement idempotente, soit porter une vraie clé d’idempotence au système en aval.
Comment tester l’idempotence avant la production
La plupart des équipes testent les chemins heureux puis s’étonnent lorsque les tentatives de reconnexion se produisent. Ce n’est pas suffisant.
Vous devriez avoir des tests automatisés pour au moins ces cas :
- le serveur engage la mutation mais la réponse n’atteint jamais le client
- deux requêtes identiques sont en concurrence avec la même clé d’idempotence
- la même clé est réutilisée avec une charge utile différente
- un consommateur engage son travail de base de données et plante avant l’accusé de réception
- un webhook est rejoué avec le même ID de livraison
- un expéditeur de boîte de sortie publie le même événement plus d’une fois
- une Activité de workflow complète l’appel externe et plante avant que la complétion ne soit signalée
- un enregistrement d’idempotence expire et une véritable tentative de reconnexion tardive arrive
AWS recommande explicitement des suites de tests complètes qui incluent des requêtes réussies, des requêtes échouées et des requêtes en double. Ce conseil est pedestral et absolument correct.
J’ajouterais un exercice de défaillance de plus. Vérifiez que la réponse rejouée est sémantiquement équivalente au premier résultat. AWS discute des tentatives de reconnexion tardives et plaide pour des réponses qui préservent le sens original même après que l’état sous-jacent a changé. C’est la différence entre « aucun effet de bord supplémentaire n’est survenu » et « l’appelant a toujours un contrat cohérent ».
Règles d’opinion qui sauvent les systèmes réels
Voici les règles que je ferais respecter dans une revue d’architecture.
Premièrement, les clés d’idempotence appartiennent à l’intention commerciale, pas aux tentatives de transport.
Deuxièmement, étendez chaque clé par locataire et opération. Les espaces de clés globaux sont la façon dont des requêtes non liées entrent en collision.
Troisièmement, persistez la décision de déduplication atomiquement avec la mutation. Si ce n’est pas vrai, la conception est fausse.
Quatrièmement, rejetez les tentatives de reconnexion de même clé et de charge utile différente. Stripe et AWS le font tous les deux pour de bonnes raisons.
Cinquièmement, conservez les clés pour l’horizon de rejou complet du processus métier, pas pour la fenêtre de file d’attente la plus courte.
Sixièmement, associez les producteurs avec une boîte de sortie et les consommateurs avec le suivi des ID de message. Un côté sans l’autre est une moitié de conception.
Septièmement, propagez la même identité d’opération en aval lorsque l’action commerciale est la même. AWS recommande explicitement de passer le jeton d’idempotence le long de la chaîne de traitement.
Huitièmement, ne supposez jamais que le marketing exactement une fois supprime le besoin d’effets de bord idempotents.
Si cela semble strict, tant mieux. L’idempotence est là où l’architecture optimiste rencontre la réalité de la production. Vous n’avez pas besoin de complexité partout. Mais partout où des effets de bord en double nuiraient à l’argent, à l’état ou à la confiance, l’idempotence devrait être une partie de première classe du contrat.