12 août 2025·8 min de lecture

Déclencheurs ou processus en arrière-plan : quel choix pour des notifications fiables ?

Apprenez quand les triggers ou les workers en arrière-plan sont plus sûrs pour les notifications, avec des conseils pratiques sur les retries, les transactions et la prévention des doublons.

Déclencheurs ou processus en arrière-plan : quel choix pour des notifications fiables ?

Pourquoi la livraison des notifications casse dans les vraies applications

Les notifications semblent simples : un utilisateur fait quelque chose, puis un e-mail ou un SMS est envoyé. La plupart des échecs réels viennent du timing et des doublons. Les messages sont envoyés avant que les données ne soient vraiment sauvegardées, ou ils sont envoyés deux fois après un échec partiel.

Une « notification » peut être beaucoup de choses : reçus par e-mail, codes à usage unique par SMS, alertes push, messages in-app, pings Slack ou Telegram, ou un webhook vers un autre système. Le problème commun est toujours le même : vous essayez de coordonner un changement en base avec quelque chose hors de votre application.

Le monde extérieur est désordonné. Les fournisseurs peuvent être lents, renvoyer des timeouts, ou accepter une requête alors que votre appli n'a jamais reçu la réponse de succès. Votre propre application peut planter ou redémarrer au milieu d'une requête. Même des envois « réussis » peuvent être rejoués à cause de retries d'infrastructure, de redémarrages de workers, ou d'un utilisateur qui appuie à nouveau sur le bouton.

Les causes courantes de livraison de notification cassée incluent timeouts réseau, pannes ou limites de débit du provider, redémarrages de l'app au mauvais moment, retries qui réexécutent la même logique d'envoi sans garde unique, et des designs où une écriture en base et un envoi externe se produisent comme une seule étape combinée.

Quand on demande des « notifications fiables », on veut généralement l'une des deux choses :

  • livrer exactement une fois, ou
  • au moins ne jamais dupliquer (les doublons sont souvent pires qu'un délai).

Obtenir à la fois rapidité et sécurité parfaite est difficile, donc on finit par choisir des compromis entre vitesse, sécurité et complexité.

C'est pourquoi le choix entre triggers et workers en arrière-plan n'est pas qu'un débat d'architecture. Il s'agit de définir quand un envoi peut avoir lieu, comment les échecs sont retentés, et comment empêcher les e-mails ou SMS en double quand quelque chose tourne mal.

Triggers et workers en arrière-plan : ce que ça veut dire

Quand on compare triggers et workers, on compare en réalité où la logique de notification s'exécute et à quel point elle est liée à l'action qui l'a provoquée.

Un trigger, c'est « fais-le maintenant quand X se produit ». Dans beaucoup d'apps, cela signifie envoyer un e-mail ou un SMS juste après une action utilisateur, dans la même requête web. Les triggers peuvent aussi être au niveau base de données : un trigger DB s'exécute automatiquement quand une ligne est insérée ou mise à jour. Les deux types donnent l'impression d'immédiateté, mais ils héritent du timing et des limites de ce qui les a déclenchés.

Un worker en arrière-plan, c'est « fais-le bientôt, mais pas au premier plan ». C'est un processus séparé qui récupère des jobs dans une file et tente de les exécuter. Votre appli principale enregistre ce qui doit se produire, puis revient rapidement, pendant que le worker gère les parties lentes et sujettes aux échecs comme l'appel à un fournisseur d'e-mails ou de SMS.

Un « job » est l'unité de travail que le worker traite. Il inclut typiquement qui notifier, quel template, quelles données, le statut courant (queued, processing, sent, failed), combien de tentatives ont eu lieu, et parfois un horaire prévu.

Un flux typique de notification ressemble à ceci : vous préparez les détails du message, vous placez un job en file, vous appelez le provider, vous enregistrez le résultat, puis vous décidez de réessayer, d'arrêter ou d'alerter quelqu'un.

Frontières de transaction : quand il est réellement sûr d'envoyer

La frontière de transaction est la ligne entre « on a essayé de l'enregistrer » et « c'est vraiment enregistré ». Tant que la base n'a pas fait commit, le changement peut encore être rollbacké. Cela compte parce que les notifications sont difficiles à reprendre en arrière.

Si vous envoyez un e-mail ou un SMS avant le commit, vous pouvez informer quelqu'un d'une action qui n'a jamais eu lieu. Un client peut recevoir « Votre mot de passe a été changé » ou « Votre commande est confirmée », puis l'écriture échoue à cause d'une contrainte ou d'un timeout. L'utilisateur est confus, et le support doit démêler la situation.

Envoyer depuis un trigger de base de données semble tentant parce que ça se déclenche automatiquement au changement de données. Le problème est que les triggers s'exécutent dans la même transaction. Si la transaction rollbacke, vous avez peut‑être déjà appelé un provider d'e-mail ou SMS.

Les triggers DB sont aussi souvent plus difficiles à observer, tester et retryer en sécurité. Et lorsqu'ils effectuent des appels externes lents, ils peuvent garder des verrous plus longtemps que prévu et compliquer le diagnostic des problèmes de base.

Une approche plus sûre est l'idée de l'outbox : enregistrez l'intention de notifier comme des données, faites le commit, puis envoyez après.

Vous effectuez le changement métier et, dans la même transaction, vous insérez une ligne outbox qui décrit le message (qui, quoi, quel canal, plus une clé unique). Après le commit, un worker lit les lignes outbox en attente, envoie le message, puis les marque comme envoyées.

Les envois immédiats restent acceptables pour des messages à faible impact, d'information, où se tromper est tolérable, comme « Nous traitons votre demande ». Pour tout ce qui doit correspondre à l'état final, attendez après le commit.

Retries et gestion des échecs : où chaque approche l'emporte

Les retries sont souvent le facteur décisif.

Triggers : rapides, mais fragiles face aux échecs

La plupart des designs basés sur des triggers n'ont pas une bonne stratégie de retry.

Si un trigger appelle un provider e-mail/SMS et que l'appel échoue, vous vous retrouvez généralement avec deux mauvaises options :

  • échouer la transaction (et bloquer la mise à jour d'origine), ou
  • ignorer l'erreur (et perdre silencieusement la notification).

Aucune n'est acceptable lorsque la fiabilité compte.

Essayer de boucler ou de temporiser dans un trigger peut empirer les choses en maintenant des transactions ouvertes plus longtemps, augmentant le temps de verrouillage et ralentissant la base. Et si la base ou l'app meurt en plein envoi, vous ne pouvez souvent pas savoir si le provider a reçu la requête.

Workers en arrière-plan : conçus pour les retries

Un worker considère l'envoi comme une tâche séparée avec son propre état. Cela facilite naturellement les retries quand il le faut.

Comme règle pratique, on réessaie généralement les échecs temporaires (timeouts, problèmes réseau transitoires, erreurs serveur, limites de débit avec attente plus longue). On ne réessaie pas les problèmes permanents (numéros invalides, e-mails malformés, rejets définitifs comme un désabonnement). Pour les erreurs « inconnues », on limite les tentatives et on rend l'état visible.

Le backoff empêche que les retries aggravent la situation. Commencez par une courte attente, puis augmentez-la à chaque fois (par exemple 10s, 30s, 2m, 10m), et arrêtez après un nombre fixe de tentatives.

Pour que cela survive aux déploiements et redémarrages, stockez l'état de retry avec chaque job : nombre de tentatives, prochain horaire de tentative, dernière erreur (brève et lisible), dernier timestamp d'essai, et un statut clair comme pending, sending, sent, failed.

Si votre app redémarre en plein envoi, un worker peut revérifier les jobs bloqués (par exemple statut = sending avec un ancien timestamp) et les retryer en sécurité. C'est là que l'idempotence devient essentielle pour qu'un retry ne provoque pas un double-envoi.

Empêcher les doublons d'e-mails et SMS avec l'idempotence

Stoppez les notifications en double
Ajoutez des clés d'idempotence pour que timeouts et retries n'entraînent pas de doublons.
Créer un projet

L'idempotence signifie que vous pouvez exécuter la même action « envoyer une notification » plusieurs fois et que l'utilisateur la reçoit une seule fois.

Le cas classique de duplication est un timeout : votre appli appelle un provider e-mail ou SMS, la requête timeoute, et votre code retente. La première requête a peut‑être en réalité réussi, donc le retry crée un doublon.

Une solution pratique est de donner à chaque message une clé stable et de traiter cette clé comme source unique de vérité. De bonnes clés décrivent ce que le message signifie, pas quand vous avez essayé de l'envoyer.

Les approches courantes incluent :

  • un notification_id généré quand vous décidez « ce message doit exister », ou
  • une clé métier dérivée comme order_id + template + recipient (seulement si cela définit vraiment l'unicité).

Puis stockez un registre d'envois (souvent la table outbox elle-même) et faites en sorte que tous les retries le consultent avant d'envoyer. Gardez des états simples et visibles : created (décidé), queued (prêt), sent (confirmé), failed (échec confirmé), canceled (plus nécessaire). La règle critique est de n'autoriser qu'un seul enregistrement actif par clé d'idempotence.

L'idempotence côté provider peut aider quand elle est supportée, mais elle ne remplace pas votre propre registre. Vous devez toujours gérer vos retries, déploiements et redémarrages de workers.

Traitez aussi les résultats « inconnus » comme prioritaires. Si une requête a timeouté, ne renvoyez pas immédiatement. Marquez-la comme en attente de confirmation et retryez en vérifiant le statut de livraison chez le provider quand c'est possible. Si vous ne pouvez pas confirmer, retardez et alertez plutôt que de renvoyer deux fois.

Un pattern sûr par défaut : outbox + worker en arrière-plan (étape par étape)

Si vous voulez un défaut sûr, le pattern outbox plus worker est difficile à battre. Il garde l'envoi en dehors de la transaction métier tout en garantissant que l'intention d'envoyer est persistée.

Le flux

Considérez « envoyer une notification » comme des données que vous stockez, pas comme une action que vous déclenchez.

Vous sauvegardez le changement métier (par exemple, le statut d'une commande). Dans la même transaction, vous insérez une ligne outbox avec destinataire, canal (email/SMS), template, payload, et une clé d'idempotence. Vous faites le commit. Ce n'est qu'après ce point que quelque chose peut être envoyé.

Un worker en arrière-plan récupère régulièrement les lignes outbox en attente, les envoie, et enregistre le résultat.

Ajoutez une étape de réclamation simple pour éviter que deux workers ne prennent la même ligne. Cela peut être un changement de statut vers processing ou un timestamp de verrouillage.

Bloquer les doublons et gérer les échecs

Les doublons arrivent souvent quand un envoi a réussi mais que votre appli a crashé avant d'enregistrer « sent ». Vous résolvez cela en rendant l'écriture « mark sent » sûre à répéter.

Utilisez une contrainte d'unicité (par exemple sur la clé d'idempotence et le canal). Retryez selon des règles claires : tentatives limitées, délais croissants, et seulement pour les erreurs retryables. Après la dernière tentative, déplacez le job dans un état mort (comme failed_permanent) pour qu'on puisse le revoir et le retraiter manuellement.

La surveillance peut rester simple : comptes de pending, processing, sent, retrying et failed_permanent, plus l'âge de la plus ancienne ligne pending.

Exemple concret : quand une commande passe de « Packed » à « Shipped », vous mettez à jour la ligne de commande et créez une seule ligne outbox avec la clé d'idempotence order-4815-shipped. Même si le worker plante en plein envoi, les reruns ne provoqueront pas d'envoi en double parce que l'écriture « sent » est protégée par cette clé unique.

Quand les workers en arrière-plan sont un meilleur choix

Construisez des flux prêts pour la production
Générez du vrai code backend et applicatif tout en gardant la logique de notification cohérente.
Construire avec AppMaster

Les triggers DB réagissent dès que les données changent. Mais si le job est « livrer une notification de manière fiable dans des conditions réelles chaotiques », les workers offrent généralement plus de contrôle.

Les workers conviennent mieux quand vous avez besoin d'envois programmés (rappels, digests), de gros volumes avec limites de débit et backpressure, de tolérance à la variabilité des providers (limites 429, réponses lentes, pannes courtes), de workflows multi-étapes (envoyer, attendre la livraison, puis relancer), ou d'événements cross-systèmes qui demandent réconciliation.

Un exemple simple : vous prélevez un client, puis envoyez un reçu SMS, puis un e-mail de facture. Si le SMS échoue à cause d'un gateway en panne, vous voulez quand même que la commande reste payée et voulez un retry sûr plus tard. Mettre cette logique dans un trigger mélange « les données sont correctes » avec « un tier est disponible maintenant ». C'est risqué.

Les workers rendent aussi l'exploitation plus simple. Vous pouvez mettre une file en pause pendant un incident, inspecter les échecs et relancer avec des délais.

Erreurs courantes qui causent des messages manqués ou doublés

Rendez les envois observables
Modélisez les jobs de notification dans PostgreSQL et gardez les retries et statuts visibles.
Commencer

La façon la plus rapide d'avoir des notifications peu fiables est de « l'envoyer simplement » là où c'est pratique, puis d'espérer que les retries sauveront la situation. Que vous utilisiez des triggers ou des workers, ce sont les détails autour des échecs et de l'état qui décident si les utilisateurs reçoivent un message, deux, ou aucun.

Un piège courant est d'envoyer depuis un trigger DB en pensant qu'il ne peut pas échouer. Les triggers s'exécutent dans la transaction DB, donc tout appel lent à un provider peut retarder l'écriture, provoquer des timeouts ou verrouiller les tables plus longtemps que prévu. Pire, si l'envoi échoue et que vous rollbackez, vous risquez de renvoyer plus tard et d'envoyer deux fois si le provider a accepté la première demande.

Erreurs fréquentes :

  • Retenter tout de la même façon, y compris les erreurs permanentes (mauvais e-mail, numéro bloqué).
  • Ne pas séparer « queued » de « sent », donc on ne sait pas ce qu'il est sûr de retryer après un crash.
  • Utiliser des timestamps comme clé de déduplication, ce qui rend l'unicité ineffective lors des retries.
  • Faire des appels aux providers dans le chemin de la requête utilisateur (checkout et envoi de formulaire ne doivent pas attendre les gateways).
  • Traiter les timeouts provider comme « non livré », alors que beaucoup sont en réalité « inconnu ».

Exemple simple : vous envoyez un SMS, le provider timeoute, et vous retentez. Si la première requête a réellement réussi, l'utilisateur reçoit deux codes. La solution est d'enregistrer une clé d'idempotence stable (comme notification_id), de marquer le message comme queued avant l'envoi, puis de ne marquer sent qu'après une réponse claire de succès.

Vérifications rapides avant de déployer les notifications

La plupart des bugs de notification ne viennent pas de l'outil. Ils viennent du timing, des retries et des enregistrements manquants.

Confirmez que vous n'envoyez qu'après que l'écriture en base est commitée. Si vous envoyez pendant la transaction et qu'elle rollbacke, les utilisateurs peuvent recevoir un message sur quelque chose qui n'est jamais arrivé.

Ensuite, donnez à chaque notification une identité unique. Donnez à chaque message une clé d'idempotence stable (par exemple order_id + event_type + channel) et appliquez-la en stockage pour qu'un retry ne crée pas une seconde notification « nouvelle ».

Avant la mise en production, vérifiez ces bases :

  • L'envoi se produit après le commit, pas pendant l'écriture.
  • Chaque notification a une clé d'idempotence unique, et les doublons sont rejetés.
  • Les retries sont sûrs : le système peut relancer le même job et n'envoyer qu'une seule fois au maximum.
  • Chaque tentative est enregistrée (statut, last_error, timestamps).
  • Les tentatives sont plafonnées, et les éléments bloqués ont un endroit clair pour être revus et retraités.

Testez le comportement au redémarrage volontairement. Tuez le worker en plein envoi, redémarrez-le, et vérifiez qu'il n'y a pas d'envois en double. Faites de même sous charge sur la base.

Un scénario simple à valider : un utilisateur change son numéro de téléphone, puis vous envoyez un SMS de vérification. Si le provider SMS timeoute, votre appli retente. Avec une bonne clé d'idempotence et un journal de tentatives, vous envoyez soit une seule fois, soit vous réessayez plus tard sans spammer.

Scénario exemple : mises à jour de commande sans double-envoi

Utilisez le pattern outbox dès aujourd'hui
Construisez un flux outbox + worker pour que les e-mails et SMS ne soient envoyés qu'après le commit.
Essayer AppMaster

Un magasin envoie deux types de messages : (1) un e-mail de confirmation de commande juste après le paiement, et (2) des SMS de suivi quand le colis est en route et livré.

Voici ce qui se passe mal quand vous envoyez trop tôt (par exemple dans un trigger DB) : l'étape de paiement écrit une ligne orders, le trigger s'exécute et e-mail le client, puis la capture du paiement échoue une seconde plus tard. Vous avez alors un e-mail « Merci pour votre commande » pour une commande qui n'a jamais existé.

Maintenant imaginez l'inverse : le statut de livraison passe à « Out for delivery », vous appelez votre provider SMS et le provider timeoute. Vous ne savez pas s'il a envoyé le message. Si vous retentez immédiatement, vous risquez deux SMS. Si vous n'essayez pas, vous risquez de n'envoyer aucun message.

Un flux plus sûr utilise une ligne outbox et un worker. L'app commit la commande ou le changement de statut, et dans la même transaction écrit une ligne outbox comme « envoyer le template X à l'utilisateur Y, canal SMS, clé d'idempotence Z ». Ce n'est qu'après commit qu'un worker délivre les messages.

Chronologie simple :

  • Le paiement réussit, la transaction commit, une ligne outbox pour l'e-mail de confirmation est sauvegardée.
  • Le worker envoie l'e-mail, puis marque l'outbox comme sent avec un ID de message provider.
  • Le statut de livraison change, la transaction commit, une ligne outbox pour la mise à jour SMS est sauvegardée.
  • Le provider timeoute, le worker marque l'outbox comme retryable et réessaie plus tard en utilisant la même clé d'idempotence.

Au retry, la ligne outbox est la source unique de vérité. Vous ne créez pas une seconde requête d'envoi, vous terminez la première.

Pour le support, c'est aussi plus clair. Ils voient les messages coincés en failed avec la dernière erreur (timeout, mauvais numéro, e-mail bloqué), combien de tentatives ont été faites, et s'il est sûr de relancer sans double-envoi.

Étapes suivantes : choisissez un pattern et implémentez-le proprement

Choisissez un défaut et documentez-le. Les comportements incohérents viennent généralement du mélange aléatoire de triggers et de workers.

Commencez petit avec une table outbox et une boucle de worker. Le premier objectif n'est pas la rapidité, mais la correction : stocker ce que vous comptez envoyer, l'envoyer après le commit, et ne marquer comme envoyé que lorsque le provider confirme.

Plan de déploiement simple :

  • Définissez les événements (order_paid, ticket_assigned) et les canaux possibles.
  • Ajoutez une table outbox avec event_id, recipient, payload, status, attempts, next_retry_at, sent_at.
  • Construisez un worker qui interroge les lignes pending, envoie et met à jour le statut en un seul endroit.
  • Ajoutez l'idempotence avec une clé unique par message et « ne rien faire si déjà envoyé ».
  • Catégorisez les erreurs en retryable (timeouts, 5xx) vs non retryable (mauvais numéro, e-mail bloqué).

Avant d'augmenter le volume, ajoutez une visibilité basique. Suivez le nombre de pending, le taux d'échec, et l'âge du plus ancien pending. Si le plus ancien pending continue de grossir, vous avez probablement un worker bloqué, une panne provider, ou un bug logique.

Si vous construisez dans AppMaster (appmaster.io), ce pattern se mappe proprement : modélisez l'outbox dans le Data Designer, écrivez la mise à jour métier et la ligne outbox dans une transaction, puis exécutez la logique send-and-retry dans un processus d'arrière-plan séparé. Cette séparation est ce qui rend la livraison des notifications fiable même quand les providers ou les déploiements se comportent mal.

FAQ

Dois-je utiliser des triggers ou des workers en arrière-plan pour les notifications ?

Les processus en arrière-plan sont généralement le choix le plus sûr parce que l'envoi est lent et sujet aux échecs, et les workers sont conçus pour gérer les retries et offrir de la visibilité. Les triggers peuvent être rapides, mais ils sont fortement couplés à la transaction ou à la requête qui les déclenche, ce qui complique la gestion propre des échecs et des doublons.

Pourquoi est-ce risqué d'envoyer une notification avant le commit en base ?

C'est risqué car l'écriture en base peut encore être annulée. Vous pouvez vous retrouver à notifier un utilisateur d'une commande, d'un changement de mot de passe ou d'un paiement qui n'a finalement pas été commis, et vous ne pouvez pas « annuler » un e-mail ou un SMS une fois envoyé.

Quel est le plus gros problème à envoyer depuis un trigger de base de données ?

Un trigger de base de données s'exécute dans la même transaction que la modification de ligne. S'il appelle un provider d'e-mail/SMS et que la transaction échoue ensuite, vous avez peut‑être envoyé un vrai message pour une modification qui n'a pas persisté, ou vous pouvez bloquer la transaction à cause d'un appel externe lent.

Qu'est-ce que le pattern outbox en termes simples ?

Le pattern outbox en termes simples : stockez l'intention d'envoyer comme une ligne dans la base de données, dans la même transaction que le changement métier. Après le commit, un worker lit les lignes outbox en attente, envoie le message et marque la ligne comme envoyée, ce qui rend le timing et les retries beaucoup plus sûrs.

Que faire lorsqu'une requête vers un provider e-mail/SMS expire (timeout) ?

Souvent l'issue réelle est « inconnue », pas « échouée ». Un bon système enregistre la tentative, attend et réessaye prudemment en utilisant la même identité de message, au lieu de renvoyer immédiatement et risquer un doublon.

Comment éviter les doublons d'e-mails ou de SMS lors des retries ?

Utilisez l'idempotence : donnez à chaque notification une clé stable qui représente ce que le message signifie (pas le moment où vous avez essayé). Stockez cette clé dans un registre (souvent la table outbox) et appliquez une règle d'une seule ligne active par clé, pour que les retries achèvent le même message au lieu d'en créer un nouveau.

Quelles erreurs dois-je réessayer et lesquelles traiter comme permanentes ?

Réessayez les erreurs temporaires comme les timeouts, les réponses 5xx ou les limitations de débit (en augmentant l'attente). N'essayez pas les erreurs permanentes comme les adresses invalides, numéros bloqués ou hard bounces ; marquez-les comme échoués et rendez-les visibles pour correction manuelle plutôt que de relancer sans fin.

Comment les workers gèrent-ils les redémarrages ou crashs en plein envoi ?

Un worker peut rechercher les jobs coincés en sending au-delà d'un délai raisonnable, les remettre en état réessayable et retenter avec backoff. Cela ne fonctionne bien que si chaque job enregistre son état (tentatives, timestamps, dernière erreur) et que l'idempotence empêche les doubles envois.

Quelles données de job dois-je conserver pour rendre la livraison observable ?

Cela signifie que vous devez pouvoir répondre à « est‑il sûr de réessayer ? ». Stockez des statuts clairs comme pending, processing, sent et failed, ainsi que le nombre de tentatives et la dernière erreur. Cela facilite le support et le debug, et permet au système de récupérer sans deviner.

Comment implémenter ce pattern dans AppMaster ?

Modélisez une table outbox dans le Data Designer, écrivez la mise à jour métier et la ligne outbox dans une même transaction, puis exécutez la logique d'envoi et de retry dans un processus d'arrière-plan séparé. Gardez une clé d'idempotence par message et enregistrez les tentatives pour que les déploiements, retries et redémarrages de worker n'entraînent pas de doublons.

Facile à démarrer
Créer quelque chose d'incroyable

Expérimentez avec AppMaster avec un plan gratuit.
Lorsque vous serez prêt, vous pourrez choisir l'abonnement approprié.

Démarrer