Idempotence des webhooks n8n : éviter de traiter deux fois la même requête
Publié le 23 juillet 2026 · 6 min de lecture
Un webhook qui se déclenche deux fois pour le même événement n'est pas un cas exotique : c'est le comportement normal de la quasi-totalité des expéditeurs sérieux. Stripe, GitHub, Typeform et la plupart des services qui envoient des webhooks fonctionnent en at-least-once delivery — si votre instance n8n ne répond pas assez vite, répond en erreur, ou que le réseau coupe la connexion avant la réponse, l'expéditeur réessaie. C'est une garantie de fiabilité de leur côté. Côté n8n, sans précaution, ça veut dire qu'un même paiement Stripe peut déclencher deux fois l'envoi d'une facture, ou qu'un même commit GitHub peut lancer deux fois un déploiement. Ce guide explique comment rendre un workflow n8n idempotent : capable d'absorber un rejeu sans dupliquer ses effets.
Pourquoi un webhook se déclenche deux fois
Trois scénarios courants, tous légitimes côté expéditeur :
- Timeout de réponse : si le workflow traite l'événement (appel LLM, écriture en base, envoi d'email) avant de répondre au webhook, et que ce traitement dépasse le délai de timeout de l'expéditeur (souvent quelques secondes), celui-ci considère la livraison échouée et renvoie la même requête.
- Réponse HTTP 5xx : une erreur transitoire côté n8n (redémarrage, pic de charge) fait répondre une erreur serveur ; l'expéditeur réessaie automatiquement, généralement plusieurs fois avec un backoff croissant.
- Plusieurs workers en mode queue : sur une instance en mode queue avec Redis, rien n'empêche qu'un rejeu de l'expéditeur et l'exécution originale atterrissent sur deux workers différents, exécutés presque simultanément.
Aucun de ces cas n'est un bug de n8n ni de l'expéditeur : c'est la conséquence directe d'un protocole de livraison fiable. Le node Webhook exécute le workflow à chaque requête reçue, sans mémoire des requêtes précédentes — la déduplication est entièrement à la charge du workflow.
Idempotence : un concept plus précis qu'« éviter les doublons »
Un traitement est idempotent quand l'exécuter plusieurs fois produit le même résultat que l'exécuter une seule fois. Ce n'est pas la même chose que bloquer purement et simplement une requête déjà vue : un webhook idempotent bien conçu peut répondre avec succès à un rejeu (souvent en renvoyant la réponse déjà calculée la première fois) sans ré-exécuter les effets de bord. Pat Helland, dans un article de référence resté une lecture classique en systèmes distribués (Idempotence Is Not a Medical Condition, ACM Queue, 2012 — voir sur Google Scholar), formalise cette distinction : la fiabilité d'un système ne vient pas de l'absence de rejeux, inévitables dans toute architecture distribuée, mais de la capacité de chaque opération à les absorber sans en altérer le résultat final. C'est exactement le principe à appliquer à un workflow n8n déclenché par webhook.
Stratégie 1 : la clé d'idempotence côté appelant
Quand vous contrôlez l'appelant (votre propre frontend, un backend interne, un autre workflow n8n), le pattern le plus robuste — popularisé par l'API Stripe — consiste à générer côté client une clé unique par intention métier (par exemple un UUID généré une seule fois par tentative d'achat, réutilisé sur chaque retry côté client) et à l'envoyer dans un header Idempotency-Key. Le workflow n8n vérifie alors si cette clé existe déjà dans une table de suivi avant de traiter quoi que ce soit :
- Webhook reçoit la requête avec le header
Idempotency-Key. - Un node Data Table ou Supabase tente d'insérer la clé dans une table dédiée, avec une contrainte d'unicité sur la colonne clé.
- Si l'insertion réussit : c'est une requête neuve, le workflow traite normalement puis stocke le résultat associé à la clé.
- Si l'insertion échoue pour cause de doublon : c'est un rejeu, le workflow renvoie directement le résultat déjà stocké, sans ré-exécuter le traitement métier.
Stratégie 2 : la déduplication par identifiant métier
Quand vous ne contrôlez pas l'appelant (Stripe, GitHub, Typeform), il n'y a généralement pas de header d'idempotence à votre disposition — mais le payload contient presque toujours un identifiant stable de l'événement lui-même (id de l'événement Stripe, delivery_id GitHub, ID de soumission Typeform). Ce champ joue exactement le même rôle qu'une clé d'idempotence explicite : il suffit de le traiter de la même façon dans une table de déduplication, sur le modèle décrit dans notre guide n8n Data Tables. C'est le même réflexe que pour le classement automatique de documents entrants ou le tri d'emails par IA : ne jamais traiter deux fois le même identifiant source.
Le piège de la race condition
Un schéma naïf — un node IF qui vérifie d'abord par un SELECT si la clé existe, puis un node d'insertion seulement si elle n'existe pas — fonctionne en test mais casse en production dès que deux requêtes arrivent presque simultanément : les deux exécutions peuvent lire « la clé n'existe pas » avant qu'aucune des deux n'ait eu le temps d'insérer. Résultat, le traitement s'exécute deux fois malgré la vérification. Ce scénario n'est pas théorique : c'est précisément ce qui arrive quand un rejeu atterrit sur un worker différent de l'exécution originale en mode queue.
La correction ne se joue pas dans la logique du workflow, mais dans la base : une colonne UNIQUE sur Supabase/Postgres, combinée à un INSERT ... ON CONFLICT DO NOTHING (ou l'opération upsert équivalente côté node Supabase), rend l'insertion atomique. Une seule des deux exécutions concurrentes réussit réellement l'insertion ; l'autre reçoit une réponse de conflit qu'elle interprète comme « déjà traité » et court-circuite ses effets de bord. L'atomicité vient de la garantie transactionnelle de la base de données, pas d'un test préalable dans un node IF.
Réduire le risque à la source : répondre avant de traiter
Une bonne partie des rejeux déclenchés par timeout disparaît simplement en réglant le node Webhook sur Respond Immediately (ou en plaçant un node Respond to Webhook juste après réception, avant tout traitement long) : l'expéditeur reçoit un 200 OK en quelques millisecondes, bien avant son propre délai de timeout, et le reste du workflow (appel LLM, écriture en base, notification) continue en arrière-plan sans risque de déclencher un nouveau retry. Ce réglage ne dispense pas de la déduplication — un rejeu reste possible pour d'autres raisons (5xx transitoire, retry manuel côté expéditeur) — mais il élimine la cause la plus fréquente en pratique.
Ce que ça change en cas d'échec réel
Un webhook idempotent bien conçu s'articule naturellement avec la gestion d'erreurs : si le traitement échoue après l'insertion de la clé de déduplication, un rejeu légitime de l'expéditeur ne doit pas rester bloqué en « déjà traité » alors que rien n'a réellement abouti. La table de suivi doit donc distinguer au minimum trois états (en cours, réussi, échoué), et seul l'état « réussi » doit court-circuiter un nouveau traitement — un état « échoué » doit au contraire laisser la porte ouverte à une nouvelle tentative, éventuellement relancée par un Error Workflow dédié plutôt que par le rejeu de l'expéditeur seul.
En résumé
Un webhook rejoué n'est pas une anomalie à corriger côté expéditeur, c'est une propriété structurelle des systèmes distribués fiables — et n8n ne filtre rien de ce côté par défaut. Une clé d'idempotence explicite (ou, à défaut, l'identifiant métier déjà présent dans le payload), une contrainte unique en base plutôt qu'un simple test préalable, et une réponse immédiate au webhook avant tout traitement long : ces trois réflexes couvrent l'essentiel des cas de doublons rencontrés en production. Les workflows des packs FlowKit qui reçoivent des événements externes — questionnaire du Pack Conformité & Audit (149 €), API question-réponse du Pack Assistant RAG (119 €) — appliquent systématiquement ce principe : mieux vaut une déduplication invisible que découvrir en production qu'un client a reçu deux fois la même facture.
FAQ
Questions fréquentes
Le node Webhook de n8n gère-t-il l'idempotence automatiquement ?
Non. Le node Webhook exécute le workflow à chaque requête reçue, sans vérifier si une requête équivalente a déjà été traitée. Si l'expéditeur (Stripe, GitHub, votre propre backend) renvoie le même événement après un timeout, n8n déclenche une seconde exécution complète, avec les mêmes effets de bord — c'est au workflow de détecter et bloquer ce doublon.
Quelle est la différence entre une clé d'idempotence et un identifiant métier ?
Une clé d'idempotence est générée par l'appelant spécifiquement pour distinguer une requête d'un rejeu de cette même requête (header Idempotency-Key, pattern popularisé par Stripe). Un identifiant métier (ID de commande, ID d'événement GitHub) existe indépendamment de la requête HTTP et sert la même fonction quand l'appelant ne fournit pas de clé dédiée : les deux permettent la déduplication, la clé d'idempotence est simplement plus explicite sur son intention.
Comment éviter une race condition entre deux workers qui traitent le même webhook en parallèle ?
Un SELECT qui vérifie l'absence d'une ligne suivi d'un INSERT laisse une fenêtre où deux exécutions simultanées peuvent toutes les deux passer le contrôle avant que l'une des deux insère. La solution est une contrainte unique en base de données (colonne UNIQUE sur Supabase/Postgres) combinée à un upsert avec gestion de conflit (ON CONFLICT DO NOTHING) : l'atomicité est garantie par la base, pas par la logique du workflow.
Bundle FlowKit Complet
269 €