Créer une file de tâches planifiées avec BullMQ et Redis

Les applications web modernes exécutent souvent des opérations lourdes qui bloqueraient l'expérience utilisateur si elles étaient traitées de manière synchrone : envois d'e-mails en masse, génération de rapports, traitement d'images ou synchronisations avec des services tiers. Lorsque ces traitements durent plusieurs secondes, voire quelques minutes, les confier au thread principal devient vite intenable. La solution consiste à déléguer ces travaux à un système capable de les exécuter en arrière-plan, de les relancer en cas d'échec et de les ordonner dans le temps.

C'est exactement le rôle de BullMQ, une bibliothèque Node.js dédiée à la gestion de files d'attente de jobs. Construite sur Redis, elle hérite de la rapidité et de la persistance de ce moteur clé-valeur, tout en proposant une API typée particulièrement appréciée en TypeScript. Les définitions de types couvrent aussi bien la production de jobs que leur consommation par un worker, ce qui réduit les erreurs de modélisation.

L'association de BullMQ et Redis séduit par sa simplicité de mise en œuvre et sa résistance aux pannes. L'installation tient en quelques commandes et la courbe d'apprentissage reste accessible à toute personne ayant manipulé des promesses. Les files peuvent être distribuées sur plusieurs machines, ouvrant la porte à une mise à l'échelle horizontale sans réécriture du code applicatif.

Dans la suite de cet article, nous allons construire un système complet de jobs planifiés. Nous verrons comment initialiser Redis et installer BullMQ, produire des jobs avec différentes options, planifier des tâches récurrentes à l'aide d'expressions cron, construire un worker résilient et observer l'état de la file en temps réel.

Préparer l'environnement Redis et Node.js

Avant toute chose, il faut s'assurer qu'une instance Redis est disponible. Sous Debian ou Ubuntu, apt install redis-server suffit, tandis que les utilisateurs de macOS passeront par brew install redis. Pour un environnement conteneurisé, docker run -d -p 6379:6379 redis:7-alpine démarre un serveur léger prêt à l'emploi. Par défaut, Redis écoute sur le port 6379 sans authentification, ce qui convient au développement local.

Côté projet Node.js, BullMQ s'installe via npm install bullmq. Une version LTS récente de Node, idéalement la 18 ou plus, est recommandée pour profiter pleinement du typage strict. Les paramètres de connexion seront stockés dans un fichier .env pour éviter de les figer dans le code, par exemple REDIS_URL=redis://127.0.0.1:6379.

Pour partager la configuration entre le producteur et le worker, mieux vaut centraliser la connexion dans un module dédié. BullMQ accepte un objet compatible avec le client ioredis, ce qui permet d'ajuster finement les timeouts, le nombre de tentatives de reconnexion et les identifiants si Redis est sécurisé. Cette approche évite les divergences entre les différentes parties du système.

Définir les files et produire des jobs

Créer une file d'attente est un jeu d'enfant : il suffit d'instancier la classe Queue en lui passant un nom et la configuration de connexion. Le nom de la file agit comme un espace de noms et regroupe logiquement les jobs d'un même domaine fonctionnel. Plusieurs files peuvent cohabiter dans la même instance Redis sans interférence, à condition de surveiller la consommation mémoire.

La méthode add permet d'insérer un job en précisant son nom, son payload et plusieurs options utiles. Le champ delay retarde l'exécution d'un nombre donné de millisecondes, ce qui sert par exemple à laisser à un utilisateur le temps d'annuler une opération sensible. Le champ priority influence l'ordre de traitement, et attempts fixe le nombre maximal de tentatives en cas d'échec.

Pour des insertions en masse, la méthode addBulk réduit les allers-retours vers Redis et améliore les performances. En TypeScript, on prend souvent le soin de typer le payload via une interface suffixée JobData, par exemple SendWelcomeEmailJobData. Cette convention documente implicitement le contrat entre le producteur et le worker, limitant les erreurs d'exécution liées à des champs manquants ou mal nommés.

Planifier des tâches récurrentes

L'un des apports majeurs de BullMQ réside dans la planification native des jobs récurrents. L'option repeat transforme un job en déclencheur périodique et accepte une expression cron classique. On peut ainsi écrire repeat: { pattern: '0 3 * * *' } pour exécuter une tâche tous les jours à trois heures du matin, ou repeat: { pattern: '*/5 9-18 * * *' } pour un déclenchement toutes les cinq minutes pendant les heures ouvrées.

Pour des besoins plus simples, BullMQ propose une option every qui prend un intervalle en millisecondes. repeat: { every: 60_000 } exécute un job chaque minute, repeat: { every: 24 * 60 * 60 * 1000 } chaque jour. Cette flexibilité évite de tirer une bibliothèque externe de cron et garantit la cohérence avec l'état de la file, même après un redémarrage du worker.

Un point subtil concerne le fuseau horaire. Les expressions cron sont interprétées dans la zone du serveur Redis, et non dans celle de l'application Node.js. L'option tz permet d'imposer une zone comme Europe/Paris. Cette précision devient indispensable pour les traitements financiers ou les rapports légaux qui doivent se déclencher à une heure réglementaire, indépendamment du déploiement sur des machines distantes.

Construire un worker robuste et gérer les erreurs

Le worker est le consommateur qui exécute réellement les jobs. La méthode process reçoit une fonction asynchrone dont la signature expose l'objet job. Cette fonction concentre la logique métier : envoi d'e-mail, génération de PDF, synchronisation API. Le worker gère automatiquement la concurrence, le verrouillage et la sérialisation pour éviter qu'un même job ne soit traité deux fois.

La gestion des erreurs mérite une attention particulière. Toute exception levée dans process est capturée par BullMQ, qui place le job dans l'état failed. Si l'option attempts a été définie, le job est replanifié après un délai calculé via une stratégie de backoff. La stratégie exponential double le délai à chaque échec, laissant à un service tiers le temps de se rétablir. Pour une logique sur mesure, customBackoff accepte une fonction arbitraire.

Pour les erreurs non récupérables, on lèvera explicitement une UnrecoverableError ou on appellera job.moveToFailed avec un message explicite. Cette distinction évite de gaspiller des tentatives sur des jobs voués à l'échec. Un worker bien conçu journalise systématiquement l'identifiant du job, le nom de la file et la durée d'exécution. Des bibliothèques comme Pino ou Winston s'intègrent naturellement pour centraliser ces traces.

Observer la file et passer en production

Une file d'attente sans supervision est source de migraines opérationnelles. L'écosystème BullMQ fournit les modules @bull-board/express et @bull-board/fastify, qui exposent une interface web élégante. Après quelques lignes de configuration, on obtient un tableau de bord listant les jobs en attente, actifs, terminés et échoués, avec la possibilité de relancer ou supprimer n'importe quel élément.

Le passage en production impose quelques garde-fous supplémentaires. Il convient de protéger l'accès à Redis par un mot de passe, voire par un tunnel TLS si l'instance est partagée. La commande CONFIG SET maxmemory-policy allkeys-lru évite qu'un Redis saturé ne supprime arbitrairement des jobs en attente. Côté Node.js, le worker doit tourner dans un processus dédié supervisé par PM2 ou systemd, pour redémarrer automatiquement après un crash. De nombreux retours d'expérience sur ces sujets sont par ailleurs publiés sur développeurweb.com, n'hésitez pas à les consulter pour affiner votre stratégie de déploiement.

Si vous souhaitez approfondir un cas d'usage précis ou proposer un sujet pour un futur tutoriel, le formulaire de contact vous met directement en relation avec l'équipe éditoriale et vous permet d'orienter les prochains contenus publiés sur le site.