L’idempotence n’est pas un en‑tête HTTP : le guide d’un CTO pour des effets « exactly‑once » en 2026

Par Diogo Hudson Dias
Two engineers discussing an idempotency architecture diagram on a whiteboard with API, database, and queue flows.

Si vous pensez que l’idempotence n’est qu’un en‑tête HTTP, vous laissez déjà filer de l’argent. Doubles prélèvements, commandes en double, e‑mails fantômes — ce ne sont pas des cas limites. C’est ce qui arrive sur des réseaux réels avec de vrais utilisateurs qui appuient sur « actualiser », de vrais SDK qui réessaient automatiquement, et désormais de vrais agents IA qui réécrivent et renvoient les requêtes. Le fil HN « Idempotency is easy until the second request is different » a parfaitement résumé la douleur. En tant que CTO, vous avez besoin d’une idempotence qui tienne quand les charges utiles dérivent, que les requêtes arrivent dans le désordre et que les systèmes en aval ne sont qu’« éventuellement » coopératifs.

L’idempotence n’est pas un feature flag. C’est une propriété du système.

L’idempotence signifie que l’application répétée de la même opération logique produit le même effet, pas seulement le même statut HTTP. Les en‑têtes aident, mais ils ne résolvent pas les conditions de concurrence, les effets de bord partiels ni les livraisons hors séquence. Stripe conserve les clés d’idempotence pendant 24 heures pour une raison ; la fenêtre de déduplication de 5 minutes d’AWS SQS FIFO existe pour une raison ; les sémantiques « exactly‑once » de Kafka sont soigneusement bornées pour une raison. Le réseau vous enverra des doublons. Votre code aussi.

D’où viennent réellement les doublons

  • Les clients mobiles et les SDK réessaient sur timeout, souvent 2 à 5 fois avec backoff. Les utilisateurs appuient aussi sur actualiser. Vous recevez deux requêtes avec des connexions TCP différentes et des charges utiles légèrement différentes (horodatages, nonces).
  • Les proxys et équilibreurs de charge réessaient sur 502/503/504. Votre appli a peut‑être traité la première requête, mais la réponse s’est perdue.
  • Les émetteurs de webhooks (paiements, logistique) renverront les événements jusqu’à recevoir un 2xx. Vous verrez des livraisons hors séquence et des doublons — garanti.
  • Les agents IA amplifient les réessais. Les boucles d’utilisation d’outils vont « ajuster » des paramètres et renvoyer des mutations quasi identiques.

Si vous ne faites pas de l’idempotence une préoccupation architecturale de premier ordre, vous choisissez les rétrofacturations et les tickets de support.

Définissez l’opération, pas l’endpoint

L’idempotence n’a de sens que rattachée à une opération logique. « Créer la commande n°123 pour le compte A avec les articles X au prix P » est une opération. « POST /orders » ne l’est pas. Modélisez d’abord la frontière de l’opération :

  • Acteur : qui est autorisé à le faire ? (utilisateur, compte, service)
  • Cible : quelle ressource est modifiée ?
  • Intention : quelle transition d’état attend‑on ? (panier → passé, autorisé → capturé)
  • Effet : quels effets irréversibles sont déclenchés ? (prélèvement, e‑mail, décrément d’inventaire)

Votre clé d’idempotence doit correspondre à cette unité logique, pas seulement à une requête. Vous ne pouvez pas dédupliquer correctement si vous ne savez pas ce que « identique » signifie.

Un cadre de décision pour CTO : sept choix que vous ne pouvez pas différer

1) Définir la portée des clés : à qui et à quoi la clé appartient‑elle ?

  • Par acteur + opération : user_id + operation_type + client_supplied_key. Évite les collisions de clés entre locataires.
  • Par ressource cible : pour des mises à jour comme « définir l’adresse de livraison », utilisez resource_id + version (concurrence optimiste) plutôt que des clés libres.
  • Par étape de workflow : « authorize » et « capture » ne doivent pas partager le même espace de clés.

2) Stockage et TTL : pendant combien de temps vous souvenez‑vous ?

  • Postgres : une seule table avec un index unique sur (actor, op, key) est durable et simple. Surcharge de 1 à 5 ms au p95 si c’est indexé et si la taille de ligne est petite (200–800 octets persistés : statut, checksum, hachage de réponse, horodatages).
  • Redis : idéal pour un dédoublonnage en première ligne avec un TTL court (par ex. 1–6 heures). À associer à Postgres pour la durabilité des résultats.
  • DynamoDB/Cosmos : un choix naturel pour des charges globales avec écritures conditionnelles. Soyez explicite sur le TTL et la capacité d’écriture.
  • Politique de TTL : alignez sur votre risque métier. Stripe documente 24 heures ; la dédup SQS FIFO est de 5 minutes ; les webhooks relivrent souvent pendant des jours. Pour les commandes et paiements, 24–72 heures est raisonnable ; pour des actions éphémères, 15–60 minutes suffisent.

3) Relecture des résultats : renvoyez‑vous les mêmes octets ou seulement le même effet ?

  • Conservez une enveloppe de réponse : statut HTTP, en‑têtes pertinents pour les clients (cache-control, ETag) et un corps JSON stable (ou un hachage + pointeur).
  • Déterminisme : supprimez les champs non déterministes (horodatages, IDs aléatoires) ou gardez‑les stables entre relectures pour éviter de perturber les clients.

4) Politique de conflit de charge utile : le problème du « la deuxième requête est différente »

  • Hachage canonique : persistez un hachage de la charge utile canonicalisée (clés triées, chaînes nettoyées, décimales normalisées).
  • En cas de divergence : retournez 409 Conflict avec le corps de réponse original et expliquez la charge utile canonique qui a « gagné ». N’acceptez jamais silencieusement une mutation différente sous la même clé.
  • Mises à niveau : si vous supportez « idempotence + patch », exigez une nouvelle clé mais autorisez un lien via un parent_operation_id.

5) Contrôle de concurrence : éliminez les races, pas seulement les doublons

  • Premier écrit atomique : utilisez un INSERT … ON CONFLICT DO NOTHING (ou conditional put) pour revendiquer une clé avant d’exécuter l’effet. Si vous perdez la course, interrogez l’enregistrement gagnant et rejouez son résultat.
  • Verrous consultatifs : pour des clés chaudes (ex. même panier), un verrou consultatif de courte durée par ressource empêche l’effet troupeau sans verrous globaux grossiers.
  • Délais d’expiration : si le détenteur d’une clé revendiquée plante, relâchez après une période de grâce et marquez l’enregistrement comme « indéterminé », forçant un chemin de réconciliation.

6) Isolation des effets de bord : le pattern outbox/inbox

  • Outbox : écrivez le changement d’état métier et un « événement à publier » dans la même transaction. Un relais publie depuis l’outbox vers Kafka/SNS et marque comme envoyé. Évite le split « DB mise à jour mais échec de publication ».
  • Inbox : pour les consommateurs (notamment les webhooks), dédupliquez sur (source, event_id) avant d’appliquer l’état. Persistez le résultat. Ne renvoyez un 2xx qu’après un commit durable.
  • E‑mails/SMS idempotents : stockez un message_id et supprimez les doublons sur une fenêtre. Les fournisseurs en aval réessaient eux aussi.

7) Limites du « exactly‑once »

  • Files : l’éditeur idempotent de Kafka et les transactions offrent un traitement exactly‑once à l’intérieur de Kafka. Dès que vous appelez une API externe, vous retombez en at‑least‑once. Concevez avec cette honnêteté.
  • Datastores : même au sein d’une seule base, le « exactly‑once » repose sur vos contraintes uniques et votre modèle de transaction. Testez la matrice d’échecs (timeout après commit, crash de processus, partition réseau).

Une implémentation de référence pragmatique

Postgres d’abord : ennuyeux, correct, suffisamment rapide

  • Table : idempotency_keys(actor_id, op, key, payload_hash, status, response_blob, created_at, updated_at, claimed_by, expires_at). Index unique sur (actor_id, op, key).
  • Flux : le client envoie Idempotency-Key. Le serveur met la charge utile sous forme canonique et calcule payload_hash. INSERT pour revendiquer. En cas de succès, faire le travail ; stocker response_blob ; définir status=completed. En cas de conflit, SELECT la ligne existante ; si payload_hash correspond et status=completed, renvoyer la réponse stockée ; si en cours, sonder avec backoff ; si le hachage diverge, renvoyer 409 avec la réponse du gagnant.
  • Chiffres : avec un JSONB de réponse sous 8 KB et un indexage correct, la latence supplémentaire p95 de 1–3 ms dans Postgres 14+ sur un matériel milieu de gamme est typique. Le coût de stockage est de quelques centaines d’octets par requête plus la taille de la réponse ; avec un TTL de 72 heures vous restez généralement sous quelques Go pour la plupart des API SaaS.

Accélération Redis : cache frontal, Postgres comme source de vérité

  • SETNX + TTL court pour réguler le chemin chaud ; si l’opération réussit, poursuivre et écrire l’enregistrement durable dans Postgres ; sinon, récupérer depuis Postgres.
  • Script Lua pour comparer atomiquement le hachage de charge utile et poser des marqueurs de revendication afin d’éviter de brèves courses.
  • Compromis : la complexité augmente, mais vous gagnez 1–2 ms au p95 sur des endpoints très chauds. Ne comptez pas sur Redis seul pour la durabilité critique métier.

Webhooks : inbox ou ça n’a pas existé

  • Table Deliveries indexée par (provider, event_id). Contrainte d’unicité + statut + pointeur vers l’effet métier.
  • Renvoyez 200 seulement après le commit de la transition d’état métier. Si vous avez besoin d’un travail long, accusez réception rapidement et déléguez à une file.
  • Réordonnancement : utilisez une version d’événement (ex. seq) et traitez en sécurité même si N+1 arrive avant N. Cela signifie que vos gestionnaires doivent être idempotents eux aussi.

Interop SQS/Kafka : soyez honnête sur les garanties

  • SQS FIFO offre une livraison ordonnée et une déduplication de 5 minutes. Concevez malgré tout des consommateurs capables de gérer doublons et relectures hors de cette fenêtre.
  • Kafka : utilisez des producteurs idempotents et des transactions pour un EOS intra‑Kafka. Lors du pont vers des systèmes externes, vous revenez en at‑least‑once avec une outbox et une inbox côté consommateur.

Gérer « même clé, charge utile différente » sans drame

C’est là que la plupart des équipes se cassent les dents. Décidez et documentez la politique :

  • Conflit strict : si le hachage diffère, retournez 409 avec la réponse originale et la charge utile canonique qui a revendiqué la clé. Enregistrez un événement d’audit structuré.
  • Mise à niveau souple : autorisez des changements mineurs (p. ex. ajout d’un champ mémo). Implémentez un hachage de schéma de charge utile qui ignore les champs non sémantiques. Nécessite de la discipline — n’en abusez pas.
  • Nouvelle intention, nouvelle clé : pour tout changement matériel (montant, articles, adresse), exigez une nouvelle clé avec un parent_operation_id optionnel. Apprenez à vos SDK à le faire par défaut.

Observabilité : on ne gère pas ce qu’on ne voit pas

  • Journalisez l’enveloppe : actor_id, op, key, payload_hash, status, winner_node, duration_ms, replayed=true/false.
  • Métriques : duplicates_blocked_per_route, in_progress_timeouts, conflict_rate, replay_rate, p95_claim_latency. Si conflict_rate explose après un déploiement, vous avez cassé la canonicalisation.
  • Traçage : propagez operation_id dans vos spans. Étiquetez les effets en aval (email_id, charge_id) pour visualiser le fan‑out et les réessais.

Les agents IA rendent l’idempotence non négociable

Les agents ne se lassent pas ; ils réessaient. Ils modifient aussi les charges utiles de façon « utile » (« montant arrondi à deux décimales »). Si vous exposez des outils internes à des agents, imposez une enveloppe de mutation :

  • operation_id : stable par intention, généré au premier plan.
  • intent_hash : hachage des champs métier canoniques (pas du formatage).
  • capabilities_version : pour pouvoir rejeter des plans anciens qui ne respectent pas les contrats actuels.
  • clés côté serveur uniquement : ne faites pas confiance aux clés fournies par des agents via des prompts ou outils. Émettez‑les par session, avec une portée définie.

Sécurité et lutte contre les abus

  • Longueur et format des clés : 16–32 octets d’aléa (base64/hex). Rejetez les clés absurdes pour éviter les abus mémoire.
  • TTL + quotas : imposez des limites par locataire sur les nouvelles clés par minute. Un flot de clés uniques est un vecteur de DoS.
  • Chiffrement au repos : les blobs de réponse peuvent contenir des données personnelles. Traitez le magasin d’idempotence comme des données applicatives, pas comme des logs.

Plan de déploiement : 30/60/90 jours

Jour 0–30 : choisir la portée, construire les rails ennuyeux

  • Identifiez les 5 endpoints de mutation les plus critiques par revenu et risque (paiements, commandes, inventaire, crédits).
  • Implémentez le pattern « Postgres d’abord » avec contraintes uniques et relecture de réponse sur ces routes.
  • Instrumentez métriques et logs dès le premier jour. Bloquez les charges utiles conflictuelles avec 409.

Jour 31–60 : raccorder webhooks et effets de bord

  • Ajoutez une déduplication inbox pour les webhooks de vos trois principaux fournisseurs. Persistez les résultats.
  • Passez les e‑mails/SMS à des envois idempotents indexés par message_id. Supprimez les doublons sur 24 heures.
  • Introduisez une outbox pour les événements de domaine diffusés vers Kafka/SNS. Validez le chemin de reprise après crash/timeout.

Jour 61–90 : durcir et passer à l’échelle

  • Ajoutez un cache frontal Redis si le p95 se dégrade après adoption. Conservez Postgres comme source de vérité.
  • Faites un exercice de chaos : injectez des timeouts après commit, des livraisons dupliquées et des événements réordonnés. Prouvez l’absence de doubles prélèvements ou de commandes fantômes.
  • Apprenez à vos SDK et intégrateurs partenaires à envoyer des clés de manière cohérente. Documentez publiquement la politique de conflit.

Arbitrages à accepter d’emblée

  • Surcharge de stockage : vous stockerez un enregistrement par mutation logique pendant 24–72 heures. C’est une assurance bon marché face à même quelques rétrofacturations.
  • Taxe de latence : attendez‑vous à 1–3 ms au p95 pour le chemin de revendication/lecture dans une base relationnelle bien réglée. Si c’est trop, vous optimisez la mauvaise couche.
  • La stricte rigueur coûte du support : renvoyer 409 pour des charges utiles en conflit crée des tickets. C’est très bien. Mieux vaut un contact support qu’une corruption silencieuse des données.
  • Tout n’a pas besoin de clé : les lectures pures et les PUT réellement idempotents (définir une valeur exacte) n’ont pas besoin de cette machinerie. Soyez sélectif.

À quoi ressemble le bon

  • Chaque route de mutation documente sa frontière d’opération et sa politique de conflit.
  • Chaque réponse inclut un operation_id pour que les clients puissent corréler les relectures.
  • Les doublons sont observables : vous pouvez afficher un tableau de bord des doublons bloqués par route et par locataire.
  • Les webhooks sont relectibles indéfiniment en toute sécurité : la table inbox impose la déduplication, les gestionnaires sont idempotents.
  • Les effets de bord pendent aux outbox, et vous pouvez tuer un worker en plein vol sans double envoi.

Une idempotence qui survit aux réessais, aux réordonnancements et au « la deuxième requête est différente » n’est pas de l’over‑engineering. C’est le minimum pour les systèmes qui déplacent de l’argent et prennent des commandes en 2026. Traitez‑la comme une exigence métier, pas comme une préférence de développeur, et vos pages — et votre équipe finance — resteront calmes.

Points clés

  • L’idempotence est une propriété du système ancrée à une opération logique, pas à un en‑tête de requête.
  • Décidez en amont de la portée, du stockage, du TTL, de la stratégie de relecture, de la politique de conflit, du contrôle de concurrence et de l’isolation des effets de bord.
  • Utilisez un magasin durable (Postgres/DynamoDB) avec contrainte d’unicité ; Redis peut accélérer mais ne doit pas être votre source de vérité.
  • Gérez « même clé, charge utile différente » avec des hachages canoniques et 409 Conflict ; n’acceptez jamais une dérive silencieuse.
  • Adoptez les patterns outbox/inbox pour rendre idempotents, en toute sécurité, les effets externes (événements, e‑mails, webhooks).
  • Instrumentez doublons, conflits et latence p95 de revendication ; testez en chaos les réessais et réordonnancements avant que vos clients ne le fassent.
  • Les agents IA augmentent les réessais et la dérive des charges utiles ; imposez une enveloppe de mutation avec des IDs d’opération stables.

Ready to scale your engineering team?

Tell us about your project and we'll get back to you within 24 hours.

Start a conversation