Les clés d'idempotence empêchent une reprise de facturer deux fois
L'affirmation Toute opération qui modifie quelque chose et peut être reprise — un paiement, une commande, un envoi de message — finira par être reprise après avoir déjà réussi, par...
L'affirmation
Toute opération qui modifie quelque chose et peut être reprise — un paiement, une commande, un envoi de message — finira par être reprise après avoir déjà réussi, parce que le réseau a laissé tomber la réponse avant que l'appelant ne la voie. Sans moyen de reconnaître la répétition, la reprise exécute l'opération une seconde fois et vous facturez le client deux fois, créez deux commandes ou envoyez deux courriels. Une clé d'idempotence est le petit mécanisme qui fait qu'une requête répétée produit le résultat d'origine au lieu d'un second effet, et elle n'est pas optionnelle sur tout ce qui touche à l'argent.
La défaillance qui garantit que cela arrive
Prenez une requête de paiement. Votre serveur la reçoit, débite la carte avec succès, et commence à envoyer la réponse — et à cet instant, la connexion du client tombe. Le débit a eu lieu ; la confirmation n'est jamais arrivée. Du côté du client, la requête semble avoir échoué ; il fait donc la chose raisonnable et reprend. Votre serveur, sans mémoire d'avoir déjà vu cette requête, débite la carte de nouveau. Le client est facturé deux fois pour un achat, et le seul relevé qu'un problème est survenu est un client confus et une rétrofacturation.
Ce n'est pas un cas limite que vous pouvez espérer éviter. Les réseaux laissent tomber des réponses couramment, les clients reprennent par conception, et les connexions mobiles y sont particulièrement sujettes. Toute opération de création ou de débit exposée à un réseau non fiable rencontrera cela, et la seule question est de savoir si vous l'avez gérée avant qu'elle n'arrive ou après qu'un client l'a remarquée.
Le mécanisme
Le client génère une clé unique pour chaque opération logique — pas chaque tentative HTTP — et l'envoie avec la requête. Le serveur enregistre la clé la première fois qu'il la voit, avec le résultat, et sur toute requête ultérieure portant la même clé il renvoie le résultat stocké au lieu de refaire l'opération.
POST /charges
Idempotency-Key: 4f3c1a90-8b2e-4e21-9c77-1d0e2a5b6f88
-- cote serveur, toute l'idee :
INSERT INTO idempotency_keys (key, status)
VALUES ($1, 'processing')
ON CONFLICT (key) DO NOTHING;
-- si l'insertion a touche 0 ligne, cette cle a deja ete vue :
-- renvoyer la reponse stockee, NE PAS debiter de nouveau.
La contrainte d'unicité sur la colonne de clé est ce qui rend cela sûr sous la concurrence : si deux copies de la même requête reprise arrivent simultanément, la base laisse exactement une gagner l'insertion et l'autre voit le conflit, de sorte que même une course ne peut pas produire deux débits.
La clé doit identifier l'opération, pas la tentative
L'erreur la plus courante est de générer la clé au mauvais endroit. Si le client crée une nouvelle clé chaque fois qu'il envoie la requête, y compris à la reprise, alors la reprise porte une clé différente et est traitée comme une nouvelle opération — ce qui déjoue tout le propos. La clé doit être générée une fois, quand l'utilisateur pose l'action, et réutilisée à chaque reprise de cette même action. Elle identifie l'intention — « cet achat unique » — pas la transmission.
// correct : cle creee au clic sur Payer, reutilisee a chaque reprise
const key = crypto.randomUUID(); // une fois, a l'intention
async function pay() {
return post('/charges', body, { 'Idempotency-Key': key }); // les reprises reutilisent la cle
}
Stockez la réponse, pas seulement la clé
Enregistrer qu'une clé a été utilisée n'est que la moitié du mécanisme ; vous devez aussi stocker ce qu'il faut renvoyer à sa réapparition. La première requête réussie sauvegarde son corps de réponse et son statut contre la clé, et la répétition renvoie exactement cela — de sorte que le client, quelle que soit la tentative qui l'atteint finalement, reçoit la même réponse qu'il aurait reçue la première fois. Sans la réponse stockée, le mieux que vous puissiez faire à une répétition est de signaler que l'opération a déjà eu lieu, ce que le client ne sait pas interpréter ; avec elle, la reprise est réellement transparente.
Gérez la requête encore en vol
Il y a un cas subtil entre « jamais vue » et « terminée » : la requête d'origine est encore en traitement quand la reprise arrive. La reprise ne doit pas lancer une seconde exécution, mais elle ne peut pas non plus renvoyer un résultat qui n'existe pas encore. La gestion propre est de marquer la clé processing à la première insertion et, quand une requête arrive pour une clé déjà dans cet état, de renvoyer un statut qui dit au client d'attendre et de reprendre sous peu plutôt que de poursuivre. Voilà pourquoi la ligne de clé porte un statut, pas seulement une présence — elle distingue terminé d'en cours, et les deux exigent des réponses différentes.
Expirez les clés, mais pas trop tôt
Les clés d'idempotence n'ont pas besoin de vivre éternellement, et les laisser s'accumuler indéfiniment transforme la table en un passif qui grossit lentement. Expirez-les après une fenêtre qui dépasse confortablement toute reprise réaliste — 24 heures est un choix courant et sûr, puisqu'aucun client légitime ne reprend un achat un jour plus tard. Gardez la fenêtre assez longue pour que chaque reprise d'une vraie opération y tombe, car une clé qui expire entre la requête d'origine et une reprise différée rouvre exactement le trou du double débit que le mécanisme était là pour fermer.
Où l'appliquer
Mettez des clés d'idempotence sur chaque opération où un doublon est nuisible et une reprise possible : débiter une carte, passer une commande, émettre un remboursement, envoyer un message transactionnel, provisionner une ressource payante. Les opérations en lecture seule n'en ont pas besoin, car répéter une lecture ne coûte rien. La règle est simple et vaut la peine d'être dite clairement : si l'exécuter deux fois ferait mal, et que le réseau pourrait la faire s'exécuter deux fois, il lui faut une clé d'idempotence — et le chemin de paiement en a besoin avant votre lancement, pas après que le premier client est facturé deux fois et que vous présentez des excuses en même temps que le remboursement.