25 déc. 2025·8 min de lecture

Fiabilité des webhooks : arrêtez de manquer les événements Stripe, GitHub et Slack

La fiabilité des webhooks évite les événements Stripe, GitHub et Slack manqués ou dupliqués grâce aux signatures, idempotence, retries et gestion dead-letter.

Fiabilité des webhooks : arrêtez de manquer les événements Stripe, GitHub et Slack

Pourquoi les handlers de webhooks échouent en production

Un webhook est un rappel : un système envoie une requête HTTP à votre serveur quand quelque chose se passe, par exemple un paiement, un push ou un nouveau message.

Sur le papier, c’est simple. En production, la fiabilité casse parce que les réseaux sont instables et les fournisseurs se protègent avec des retries. Le même événement peut arriver deux fois, arriver en retard ou sembler n’être jamais arrivé.

Les équipes rencontrent généralement quelques modes d’échec récurrents :

  • Événements manquants : votre endpoint a expiré, planté ou été momentanément indisponible.
  • Doublons : le fournisseur a retenté et vous avez traité le même événement une nouvelle fois.
  • Livraison hors ordre : l’événement B arrive avant A, même si A s’est produit en premier.
  • Traitement partiel : vous avez écrit en base, puis échoué avant de répondre, et on vous a renvoyé l’événement.

La plupart des fournisseurs garantissent seulement une livraison « au moins une fois », pas « exactement une fois ». Ils vont essayer fort de livrer, mais ne peuvent pas garantir un timing parfait, un ordre parfait ni une unique livraison.

L’objectif n’est donc pas de rendre les webhooks parfaits. L’objectif est d’obtenir un résultat correct même si les requêtes arrivent en double, en retard ou hors ordre. Le reste de ce guide se concentre sur quatre défenses qui couvrent la plupart des échecs réels : l’idempotence, la vérification des signatures, des retries sensés et un chemin dead-letter pour rendre les échecs visibles au lieu de silencieux.

Ce que Stripe, GitHub et Slack feront à votre endpoint

Les fournisseurs de webhooks sont polis, mais pas patients. Ils enverront un événement, attendront un court instant, et si votre endpoint ne répond pas comme attendu, ils réessaieront. C’est un comportement normal.

Attendez-vous à ce qui suit :

  • Timeouts : votre endpoint met trop de temps, ils considèrent que c’est un échec.
  • Retries : ils renvoient le même événement, parfois plusieurs fois.
  • Rafales : une journée calme devient 200 événements en une minute.
  • Pannes temporaires : votre serveur renvoie un 500, un déploiement redémarre un worker, ou DNS a un bruit.
  • Retards de livraison : des événements arrivent plusieurs minutes plus tard que prévu.

La livraison en double surprend beaucoup d’équipes. Même si votre code a fait ce qu’il fallait, le fournisseur peut ne pas le savoir. Si votre handler expire, renvoie un non-2xx ou coupe la connexion tôt, le même événement peut revenir. Si vous traitez chaque livraison comme nouvelle, vous pouvez facturer deux fois, appliquer deux fois une montée en gamme, envoyer des emails en double ou créer des enregistrements dupliqués.

L’ordre n’est pas garanti non plus. Vous pouvez voir « subscription.updated » avant « subscription.created », ou une édition de message Slack avant la création du message original, selon les retries et les routes réseau. Si votre logique suppose une séquence propre, vous pouvez écraser des données récentes par des données plus anciennes.

Cela empire quand votre handler dépend d’un travail en aval lent comme une écriture en base de données, l’envoi d’un email ou l’appel d’une autre API. Un échec réaliste ressemble à ceci : votre code attend un fournisseur d’email 8 secondes, l’envoyeur de webhook expire à 5 secondes, retente, et maintenant deux requêtes font une course pour mettre à jour le même enregistrement.

Un bon handler traite les webhooks comme des livraisons peu fiables : accepter rapidement, vérifier, dédupliquer, et traiter de manière contrôlée.

Idempotence : la solution qui évite le double traitement

Idempotence signifie ceci : si le même événement webhook frappe votre serveur deux fois (ou dix fois), votre système finit dans le même état que si vous l’aviez traité une seule fois. C’est important car les retries sont normaux.

En pratique, l’idempotence, c’est dédupliquer avec mémoire. Quand un événement arrive, vous vérifiez si vous l’avez déjà traité. Si oui, vous renvoyez un succès et ne faites rien d’autre. Si non, vous le traitez et enregistrez que vous l’avez fait.

Ce dont vous avez besoin pour dédupliquer

Il ne faut pas grand-chose, mais il faut quelque chose de stable :

  • ID d’événement fourni par le fournisseur (idéal quand disponible)
  • Nom du fournisseur (Stripe vs GitHub vs Slack)
  • Quand vous l’avez vu pour la première fois (utile pour le nettoyage et le debug)
  • Statut de traitement (reçu, traité, échoué)

Stockez cela de façon durable. Une table de base de données est le défaut le plus sûr. Un cache avec TTL peut suffire pour des événements à faible risque, mais il peut oublier lors de redémarrages ou d’évictions. Pour l’argent ou les changements d’accès, traitez l’enregistrement de déduplication comme faisant partie de vos données.

Combien de temps garder les clés ? Plus longtemps que la fenêtre de retry du fournisseur et plus longtemps que vos propres retries retardés. Beaucoup d’équipes conservent 7 à 30 jours, puis expirent les enregistrements anciens.

Effets secondaires à protéger

L’idempotence protège vos actions les plus risquées : facturer deux fois, envoyer des emails en double, upgrader un rôle deux fois, rembourser deux fois, ou créer des tickets dupliqués. Si vous ne faites qu’une seule amélioration de fiabilité cette semaine, faites celle-ci.

Vérification de signature sans pièges

La vérification de signature empêche du trafic internet aléatoire de se faire passer pour Stripe, GitHub ou Slack. Sans elle, n’importe qui peut frapper votre URL de webhook et déclencher des actions comme « marquer la facture payée » ou « inviter un utilisateur dans un workspace ». Des événements usurpés peuvent sembler suffisamment valides pour tromper des contrôles JSON basiques.

Ce qu’on vérifie est généralement le même chez les fournisseurs : le corps brut de la requête (octets exacts), un timestamp (pour bloquer les replays), un secret partagé et l’algorithme attendu (souvent un HMAC). Si l’un de ces éléments change même légèrement, la signature ne correspondra pas.

Le piège qui casse le plus souvent les intégrations réelles : parser le JSON avant de vérifier. Beaucoup de frameworks parsèment puis ré-sérialisent le corps, ce qui change les espaces ou l’ordre des clés. Votre code vérifie alors une chaîne différente de celle que le fournisseur a signée, et vous rejetez de vrais événements.

Autres erreurs fréquentes :

  • Utiliser le mauvais secret (test vs production, ou le secret du mauvais endpoint).
  • Ignorer la tolérance sur le timestamp, puis rejeter des événements valides quand l’horloge du serveur dérive.
  • Vérifier le mauvais header (certains fournisseurs envoient plusieurs versions de la signature).
  • Retourner 200 même quand la vérification échoue, ce qui rend le debug pénible.

La gestion d’erreur sûre est simple : si la vérification échoue, rejetez vite et n’exécutez aucune logique métier. Retournez une erreur claire (souvent 400, 401 ou 403 selon les attentes du fournisseur). Logguez uniquement ce qui aide au diagnostic : nom du fournisseur, ID d’événement (si présent), une courte raison comme « mauvaise signature » ou « timestamp trop ancien », et votre propre request ID. Évitez de logger les corps bruts ou tous les headers car ils peuvent contenir des secrets.

Une architecture simple de webhook qui tient la charge

Le pattern le plus fiable est ennuyeux : faites le minimum dans la requête HTTP, puis déléguez le vrai travail à un worker en arrière-plan.

Le chemin de requête sûr et rapide

Quand Stripe, GitHub ou Slack appelle votre endpoint, gardez le chemin de requête court et prévisible :

  • Vérifier la signature et les headers basiques (rejeter vite si invalide)
  • Enregistrer l’événement et une clé d’événement unique
  • Enqueue un job (ou écrire une ligne dans une « inbox »)
  • Retourner un 2xx immédiatement

Retourner un 2xx rapidement est important car les expéditeurs de webhooks retentent sur timeouts et erreurs 5xx. Si vous faites du travail lent (fan-out base, appels API, email) avant de répondre, vous augmentez les retries, les livraisons dupliquées et les rafales pendant les incidents.

Séparer l’ingestion du traitement

Pensez à deux composants :

  • Endpoint d’ingestion : vérifications de sécurité, validation minimale, enqueue, 2xx
  • Worker : logique métier idempotente, retries et mises à jour d’état

Cette séparation garde votre endpoint stable sous charge parce que le worker peut scaler et retry sans bloquer de nouveaux événements. Si Slack envoie une rafale pendant une importation d’utilisateurs, l’endpoint reste rapide tandis que la file absorbe le pic.

Pour le logging, capturez ce dont vous avez besoin pour déboguer sans exposer secrets ou données personnelles : type d’événement, expéditeur (Stripe/GitHub/Slack), delivery ID, résultat de la vérification de signature, statut de traitement et timestamps. Évitez de vider tous les headers ou corps dans les logs ; stockez les payloads seulement dans un store d’événements protégé si vous en avez vraiment besoin.

Étape par étape : un pattern de handler de webhook à copier

Clean up risky webhook code
Remove exposed secrets, tighten logging, and close easy-to-miss security gaps.

La plupart des bugs de webhook arrivent parce que le handler essaie de tout faire dans la requête HTTP. Traitez la requête entrante comme une étape de reçu, puis déplacez le vrai travail vers un worker.

Le request handler (rapide et strict)

Ce pattern fonctionne dans n’importe quelle stack :

  1. Validez la requête et capturez le corps brut. Vérifiez la méthode, le chemin attendu et le content-type. Sauvez les octets bruts avant tout parsing JSON pour que la vérification de signature ne casse pas.
  2. Vérifiez la signature tôt. Rejetez les signatures invalides avec une réponse 4xx claire. Ne « devinez » pas le contenu du payload.
  3. Extraire un ID d’événement et construire une clé d’idempotence. Préférez l’ID d’événement du fournisseur. S’il n’existe pas, construisez une clé à partir de champs stables (source + timestamp + action + objet ID).
  4. Écrivez un enregistrement d’idempotence avant les effets de bord. Faites un insert atomique du type « event_id pas encore vu ». S’il existe déjà, retournez 200 et arrêtez.
  5. Enqueuez le travail et retournez 200 rapidement. Mettez l’événement (ou un pointeur vers le payload stocké) dans une file. La requête web ne devrait pas appeler des APIs tierces, envoyer des emails ou faire du travail lourd.

Le worker (effets secondaires sûrs)

Le worker charge l’événement en file, exécute la logique métier et met à jour l’enregistrement d’idempotence vers un état clair comme processing, succeeded ou failed. Les retries appartiennent au worker, avec backoff et un cap.

Exemple : un webhook de paiement Stripe arrive deux fois. La seconde requête trouve la même event ID, voit l’enregistrement d’idempotence existant et sort sans surupgrader le client.

Retries qui aident au lieu d’empirer

Les retries sont utiles quand l’échec est temporaire. Ils sont nuisibles quand ils transforment un bug réel en pic de trafic, ou quand ils répètent une requête qui ne devrait jamais réussir.

Retry seulement quand il y a de bonnes chances que la tentative suivante fonctionne : timeouts réseau, resets de connexion et réponses 5xx de vos dépendances. Ne retenter pas les réponses 4xx qui signifient « la requête est erronée » (signature invalide, JSON invalide, champs requis manquants). Ne retenter pas non plus quand vous savez déjà que l’événement est dupliqué et géré en toute sécurité par l’idempotence.

Un jeu de règles simple :

  • Retry : timeouts, 429 rate limits, 500-599, erreurs DNS/connexion temporaires
  • Ne pas retry : 400-499 (sauf 429), signature invalide, validation de schéma échouée
  • Traiter comme succès : événement déjà traité (replay idempotent)
  • Arrêter vite : dépendance en panne pour tout le monde (utiliser un circuit breaker)
  • Toujours : limiter le nombre de tentatives et la fenêtre temporelle totale

Utilisez un backoff exponentiel avec jitter. En clair : attendez un peu, puis de plus en plus longtemps à chaque essai, et ajoutez un petit délai aléatoire pour éviter que tous les retries ne tombent en même temps. Par exemple : 1s, 2s, 4s, 8s, plus ou moins 20 % d’aléatoire.

Fixez à la fois un nombre maximal de tentatives et une fenêtre maximale de retry. Un point de départ pratique : 5 tentatives sur 10 à 15 minutes. Cela évite les boucles de retry « infinies » qui cachent des problèmes jusqu’à l’explosion.

Rendez les appels en aval sûrs. Mettez des timeouts courts sur les appels base et API, et ajoutez un circuit breaker pour arrêter d’appeler un service en échec pendant une minute ou deux.

Enfin, enregistrez pourquoi vous avez retried : timeout, 5xx, 429, nom de la dépendance et durée. Ces tags transforment « on rate parfois des webhooks » en un problème corrigeable.

Dead-letter handling : comment arrêter de perdre des événements pour toujours

Audit AI-generated integrations
If Lovable, Bolt, v0, Cursor, or Replit generated it, we’ll diagnose what breaks in production.

Il vous faut un plan pour les événements qui ne passeront pas même après retries. Une dead-letter queue (DLQ) est une zone de stockage pour les livraisons webhook qui continuent d’échouer, afin qu’elles ne disparaissent pas dans les logs ou ne restent bloquées en retry indéfini.

Un bon enregistrement DLQ contient assez de contexte pour debuguer et rejouer sans deviner :

  • Payload brut (en texte) et JSON parsé
  • Headers nécessaires pour vérification et traçage (signature, event ID, timestamp)
  • Message d’erreur et stack trace (ou une raison d’échec courte)
  • Compte d’essais et timestamps pour chaque tentative
  • Votre statut interne de traitement (utilisateur créé, plan mis à jour, etc.)

Puis rendez le replay sûr. Les replays doivent passer par le même chemin idempotent que le webhook « live », en utilisant une clé d’événement stable (généralement l’ID d’événement du fournisseur). Ainsi, rejouer un événement deux fois ne fait rien la seconde fois.

Un workflow simple aide les équipes non techniques à agir rapidement sans toucher au code. Par exemple, quand un événement de paiement échoue à cause d’une panne de base temporaire, quelqu’un peut le rejouer une fois le système rétabli.

Gardez le workflow minimal :

  • Rediriger automatiquement les échecs répétés vers la DLQ après N tentatives
  • Afficher un court message « ce qui a échoué » plus un résumé du payload
  • Autoriser le replay (avec idempotence appliquée)
  • Autoriser « marquer comme ignoré » avec une note requise
  • Escalader vers l’équipe d’ingénierie si la même erreur se répète

Fixez la rétention et les alertes. Conservez les éléments DLQ suffisamment longtemps pour couvrir week-ends et vacances (souvent 7 à 30 jours), et alertez un propriétaire clair quand la DLQ dépasse un petit seuil.

Exemple : empêcher les upgrades en double depuis un webhook de paiement Stripe

Un flux Stripe courant : un client paie, Stripe envoie un événement payment_intent.succeeded, et votre app upgrade le compte.

Voici comment ça casse. Votre handler reçoit l’événement, puis essaie de mettre à jour la base et d’appeler une fonction de facturation. La base ralentit, la requête expire, et votre endpoint renvoie un 500. Stripe suppose que la livraison a échoué et retente. Maintenant le même événement vous atteint de nouveau, et l’utilisateur est upgradé deux fois (ou reçoit deux crédits, deux factures marquées payées, ou deux emails de bienvenue).

La solution est en couches :

D’abord, vérifiez la signature Stripe avant toute autre chose. Si la signature est mauvaise, retournez 400 et stoppez.

Ensuite, rendez le traitement idempotent en utilisant le event.id Stripe. Stockez un enregistrement comme processed_events(event_id) avec une contrainte d’unicité. Quand l’événement arrive :

  • Si event_id est nouveau, acceptez-le.
  • Si event_id existe déjà, retournez 200 et ne faites rien.

Puis scindez la réception du travail : validez + enregistrez + enqueuez, puis laissez un worker effectuer l’upgrade. L’endpoint répond vite, donc les timeouts sont rares.

Enfin, ajoutez un chemin dead-letter. Si le worker échoue à cause d’une erreur de base, sauvegardez le payload et la raison d’échec pour un replay sûr. Le replay doit relancer le même code worker, et l’idempotence garantit qu’il n’y aura pas de double-upgrade.

Après ces changements, l’utilisateur voit un seul upgrade, moins de délais et beaucoup moins de tickets support « j’ai payé deux fois ».

Erreurs courantes qui créent des bugs silencieux de webhook

La plupart des bugs de webhook ne sont pas bruyants. Votre endpoint renvoie 200, les dashboards semblent OK, et des semaines plus tard vous remarquez des upgrades manquants, des emails en double ou des enregistrements désynchronisés.

Une erreur classique est de casser la vérification de signature accidentellement. Beaucoup de fournisseurs signent le corps brut, mais certains frameworks parsèment le JSON et changent les espaces ou l’ordre des clés. Si vous vérifiez contre le corps parsé, de bonnes requêtes peuvent sembler falsifiées et être rejetées. La solution : vérifier sur les octets bruts exactement tels que reçus, puis parser.

Un autre échec silencieux survient quand vous retournez 200 trop tôt. Si vous accusez réception puis que votre traitement échoue (écriture DB, appel tiers, enqueue), le fournisseur ne retentera pas parce que vous avez déjà dit que tout a marché. N’accusez le succès qu’après avoir enregistré l’événement de manière sûre (ou l’avoir enqueued).

Faire du travail lent dans le thread de requête est aussi un tueur de fiabilité. Les expéditeurs de webhooks ont souvent des timeouts courts. Si vous exécutez une logique lourde ou des appels réseau avant de répondre, vous aurez des retries, des doublons et des événements parfois manqués.

Les bugs de déduplication peuvent être subtils aussi. Si vous dédupliquez sur la mauvaise clé (par exemple user ID ou repository ID), vous risquez de rejeter de vrais événements. La déduplication doit se baser sur l’identifiant unique de l’événement (et parfois le type d’événement), pas sur l’objet concerné.

Enfin, soyez prudent avec les logs. Vider les payloads complets peut exposer des secrets, des tokens, des emails ou des IDs internes. Logguez un contexte minimal (event ID, type, timestamps) et redactez les champs sensibles.

Checklist rapide : votre intégration webhook est-elle sûre maintenant ?

Refactor webhook handlers
We untangle spaghetti logic so you can safely handle out-of-order and late events.

Un handler webhook est « sûr » lorsqu’il reste correct malgré les doublons, les retries, des bases lentes et des requêtes parfois erronées.

Commencez par les bases qui empêchent la fraude et le double traitement :

  • Vérifiez la signature en utilisant le corps brut de la requête (avant tout parsing JSON ou transformation du corps).
  • Créez et stockez un enregistrement d’idempotence avant les effets de bord. Sauvez l’ID d’événement (ou une clé calculée) d’abord, puis faites le travail.
  • Retournez un 2xx rapide dès que la requête est vérifiée et mise en file de façon sûre.
  • Mettez des timeouts clairs partout. Handler, appels DB, appels API sortants.
  • Ayez des retries avec backoff et un nombre max de tentatives. Les retries doivent ralentir dans le temps et s’arrêter après une limite.

Ensuite, vérifiez que vous pouvez récupérer quand quelque chose échoue :

  • Le stockage dead-letter existe et inclut payload, headers nécessaires, raison d’erreur et compte d’essais.
  • Le replay est réel. Vous pouvez relancer un événement dead-letter après correction, et l’idempotence empêche les effets en double.
  • La surveillance basique est en place : compte d’événements reçus, traités, retryés et dead-letterés, plus une alerte quand la DLQ grossit.

Test rapide intuitif : si votre serveur redémarre en plein milieu d’une requête, perdriez-vous l’événement ou le traiteriez-vous deux fois ? Si vous n’êtes pas sûr, corrigez ça en premier.

Prochaines étapes si vos webhooks sont déjà fragiles

Si vous avez déjà des événements manquants ou des doublons étranges, considérez ceci comme un petit projet de réparation, pas un patch rapide. Choisissez une intégration (Stripe, GitHub ou Slack) et corrigez-la de bout en bout avant de toucher aux autres.

Ordre pratique d’opérations :

  • Ajoutez d’abord la vérification de signature et rendez les échecs visibles dans les logs.
  • Rendez le traitement idempotent (stocker un event ID et ignorer les répétitions).
  • Séparez « recevoir » de « traiter » (ack rapide, travail en background).
  • Ajoutez des retries sûrs avec backoff pour les échecs temporaires.
  • Ajoutez la gestion dead-letter pour que les événements échoués soient sauvegardés et examinables.

Puis écrivez un petit plan de tests que vous pouvez exécuter à chaque changement de code :

  • Livraison dupliquée : envoyez le même événement deux fois et confirmez qu’il n’est appliqué qu’une seule fois.
  • Signature invalide : confirmez que la requête est rejetée et que rien n’est traité.
  • Événements hors ordre : confirmez que votre système reste cohérent.
  • Downstream lent : simulez un timeout pour confirmer que les retries se passent en toute sécurité.

Si vous avez hérité d’un code de webhook généré par un outil IA et qu’il semble fragile (difficile à suivre, effets secondaires surprenants, secrets exposés), une passe de remédiation focalisée est souvent plus rapide que de chasser les symptômes. FixMyMess (fixmymess.ai) aide les équipes à transformer des prototypes générés par IA en code prêt pour la production en diagnostiquant les problèmes de logique, en renforçant la sécurité et en reconstruisant des flux de webhook fragiles en un pattern d’ingestion + worker plus sûr.

Questions Fréquentes

Why do I get the same webhook event more than once?

Treat duplicates as normal, not as an edge case. Most providers deliver webhooks at least once, so a timeout or a brief 500 can cause the same event to be sent again even if your code already ran once.

When should my webhook endpoint return 200?

Return a 2xx only after you’ve verified the signature and safely recorded the event (or queued it) in a way you can recover from. If you return 200 and then your database write or enqueue fails, the provider will assume everything worked and won’t retry, which creates silent data loss.

How do I prevent double-charging or double-upgrading from retries?

Use idempotency based on a stable unique key, ideally the provider’s event ID. Store that key in durable storage with a uniqueness constraint, and if you see it again, exit early while still returning success so retries stop.

How do I handle out-of-order webhook events without corrupting state?

Don’t assume ordering, even within the same provider. Make updates conditional on versioning, timestamps, or current state so an older event can’t overwrite newer data, and design handlers so each event is safe to apply even if it arrives late.

Why does signature verification fail even when the secret is correct?

Verify against the raw request body bytes exactly as received, before any JSON parsing or re-serialization. Many frameworks change whitespace or key order during parsing, and that small change is enough to make a correct signature look wrong.

Should my webhook handler do the business logic in the request thread?

A good default is to verify the signature, write an inbox/idempotency record, enqueue work, and respond immediately. Slow work like email, third-party API calls, or heavy database fan-out belongs in a worker so the provider doesn’t time out and retry.

Which failures should I retry, and which should I not retry?

Retry when the failure is likely temporary, such as timeouts, network errors, 429 rate limits, or dependency 5xx responses. Don’t retry bad signatures or invalid payloads, and always cap attempts and total retry time so failures become visible instead of looping forever.

What should I store for deduplication, and how long should I keep it?

Record a dedupe key, when you first saw it, and a processing status so you can tell received versus completed versus failed. For anything involving money or access changes, keep the dedupe record durable and long enough to cover provider retries and your own delayed reprocessing.

What is a dead-letter queue and when do I need one?

A dead-letter path is where events go after retries are exhausted so they don’t disappear. Store enough context to understand the failure and replay safely, and make sure replay goes through the same idempotent processing so replays can’t create duplicate side effects.

My webhooks are brittle and were generated by an AI tool—what’s the fastest way to fix them?

Usually you’re missing one of the core safety layers: signature verification, idempotency, fast acknowledgment with background processing, controlled retries, or dead-letter visibility. If the code was generated by an AI tool and is hard to reason about, FixMyMess can audit the webhook flow, fix the logic and security issues, and rebuild it into an ingest-and-worker pattern quickly.