Symfony Messenger vs PHP Enqueue : différences et quel outil choisir
Par Louis-Arnaud Catoire
Le messaging asynchrone est devenu un pilier des applications PHP modernes. Le principe est simple : au lieu de bloquer une requête HTTP pendant qu'un traitement lourd s'exécute (génération de PDF, envoi de mails, calculs intensifs), on dépose un message dans une file d'attente. Un worker, processus indépendant, le dépile et le traite en arrière-plan. Le client reçoit sa réponse immédiatement.
Deux outils dominent cet espace dans l'écosystème Symfony : Symfony Messenger, le composant officiel, et PHP Enqueue, une bibliothèque indépendante plus ancienne. Les deux répondent au même besoin fondamental, mais avec des philosophies très différentes. Cet article va au-delà du simple comparatif pour aborder les implications architecturales de chaque choix.
Messenger : le bus de messages intégré
Messenger s'installe en une commande et s'intègre nativement à Symfony :
composer require symfony/messenger
Sa configuration tient dans messenger.yaml. On y déclare des transports (où stocker les messages) et un routage (quel message va dans quel transport) :
framework:
messenger:
transports:
async:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
retry_strategy:
max_retries: 3
delay: 1000
multiplier: 2
routing:
'App\Message\GenererRapportMessage': async
Un message est un simple objet PHP. Un handler le traite :
namespace App\Message;
class GenererRapportMessage
{
public function __construct(
private int $rapportId,
private string $format = 'pdf',
) {}
public function getRapportId(): int
{
return $this->rapportId;
}
public function getFormat(): string
{
return $this->format;
}
}
namespace App\MessageHandler;
use App\Message\GenererRapportMessage;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
#[AsMessageHandler]
class GenererRapportHandler
{
public function __invoke(GenererRapportMessage $message): void
{
}
}
Le dispatch passe par le MessageBusInterface :
$this->bus->dispatch(new GenererRapportMessage(42, 'xlsx'));
La force de Messenger réside dans sa simplicité conceptuelle. Un POPO comme message, un callable comme handler, un bus comme médiateur. Pas de dépendance vers un protocole de messaging spécifique.
Enqueue : la boîte à outils multi-protocole
Enqueue prend une approche différente. Là où Messenger abstrait le messaging derrière un bus applicatif, Enqueue expose les primitives des protocoles de messaging : Message, Queue, Topic, Producer, Consumer.
composer require enqueue/enqueue-bundle enqueue/fs
La configuration repose sur un système de factories dans enqueue.yaml :
enqueue:
default:
transport:
dsn: '%env(ENQUEUE_DSN)%'
client: ~
Le dispatch d'un message est plus proche du protocole sous-jacent :
use Enqueue\Client\ProducerInterface;
$producer->sendEvent('generer_rapport', json_encode([
'rapport_id' => 42,
'format' => 'xlsx',
]));
Enqueue brille par sa proximité avec les standards de messaging. Si votre système doit communiquer avec des consumers écrits en Java, Go ou Python, Enqueue produit des messages compatibles avec les protocoles natifs du broker. C'est aussi la bibliothèque qui offre le plus large catalogue de transports natifs : AMQP, Kafka, Redis, SQS, Google PubSub, Stomp, Amazon Kinesis, Wamp, filesystem, MongoDB, Doctrine...
Messenger couvre les transports les plus courants (AMQP, Redis, Doctrine, SQS) et compense avec une interface TransportInterface qui permet d'écrire un transport custom en quelques dizaines de lignes.
Pipeline de middlewares et sérialisation
Le pipeline Messenger
Ce qui distingue profondément Messenger est son pipeline de middlewares. Chaque message passe par une chaîne configurable : validation, transaction Doctrine, logging, ajout de stamps. Ce pipeline est le même mécanisme utilisé en synchrone et en asynchrone, ce qui garantit un comportement identique quel que soit le mode d'exécution.
En pratique, c'est un levier puissant. Vous pouvez injecter un middleware qui wrappe chaque handler dans une transaction Doctrine (doctrine_transaction), un autre qui valide le message avant son traitement, un autre qui mesure les temps d'exécution. L'ordre des middlewares est explicite dans la configuration.
La sérialisation, un piège classique
Quand un message traverse un transport (RabbitMQ, Redis, SQS), il doit être sérialisé. Messenger utilise par défaut le Serializer de Symfony (JSON ou XML). Enqueue sérialise en JSON brut.
Le piège apparaît lors des déploiements. Un message sérialisé avec la version N de votre classe peut être désérialisé par la version N+1, où la structure a changé. Avec Messenger, vous pouvez configurer un serializer custom qui gère la rétrocompatibilité. C'est un point que beaucoup d'équipes découvrent en production, quand des messages deviennent illisibles après un déploiement.
Règle d'or : ne jamais renommer ou supprimer une propriété de message sans avoir vidé la queue au préalable, ou sans avoir mis en place un mécanisme de versioning.
Stratégies de retry et dead letter queues
Retry avec Messenger
Messenger propose une stratégie de retry déclarative :
framework:
messenger:
failure_transport: failed
transports:
async:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
retry_strategy:
max_retries: 3
delay: 1000
multiplier: 2
max_delay: 60000
failed: 'doctrine://default?queue_name=failed'
Après épuisement des retries, le message atterrit dans le failure transport. On peut l'inspecter et le rejouer :
php bin/console messenger:failed:show
php bin/console messenger:failed:retry
Retry avec Enqueue
Enqueue délègue davantage au broker. La DelayRedeliveredMessageExtension gère les re-tentatives, mais la configuration est plus manuelle. En contrepartie, Enqueue exploite les dead letter queues natives du broker (DLX sur RabbitMQ, redrive policy sur SQS), ce qui offre plus de contrôle au niveau infrastructure.
Quelle approche pour quel contexte ?
Si votre équipe est principalement composée de développeurs PHP et que l'ops est limitée, le retry déclaratif de Messenger est plus accessible. Si vous avez une équipe infra qui maîtrise RabbitMQ ou SQS, les DLQ natives exploitées par Enqueue offrent plus de finesse.
Monitoring et observabilité
Enqueue fournit un module de monitoring intégré : nombre de messages envoyés, traités, échoués, nombre de workers actifs, consommation mémoire. Il s'intègre avec Datadog, Grafana et d'autres plateformes via des extensions dédiées.
Messenger n'a pas de monitoring intégré, mais expose des événements (WorkerMessageReceivedEvent, WorkerMessageHandledEvent, WorkerMessageFailedEvent) que vous pouvez écouter pour alimenter Prometheus, StatsD ou tout autre collecteur. Le Profiler Symfony affiche les messages dispatchés en développement. En production, il faut construire cette couche vous-même, ce qui demande un effort initial mais offre un contrôle total.
Choisir son broker : un choix d'architecture
Le choix entre Messenger et Enqueue est en réalité secondaire par rapport au choix du broker. C'est le broker qui détermine les garanties de livraison, la scalabilité et la résilience de votre système.
Doctrine comme transport : acceptable, pas idéal
Les deux outils supportent Doctrine comme transport. C'est pratique pour démarrer : pas d'infrastructure supplémentaire. Mais en production, sous charge, les limites apparaissent vite. La table messenger_messages subit des SELECT ... FOR UPDATE qui provoquent des locks. Avec MySQL 5.7 et des index sur queue_name et available_at, nous avons rencontré des deadlocks systématiques (Deadlock found when trying to get lock; try restarting transaction). La solution de contournement : lancer des workers avec un --time-limit court (60 secondes) et les relancer toutes les minutes via crontab, pour absorber les crashs liés aux deadlocks. MySQL 8.0 améliore la situation, mais ne l'élimine pas.
RabbitMQ, Redis, SQS : critères de sélection
RabbitMQ est le choix par défaut pour la plupart des projets. Il gère nativement les dead letter exchanges, le routing complexe, les priorités de messages et la persistance. Il supporte des dizaines de milliers de messages par seconde sans broncher. En revanche, il nécessite une maintenance ops (clustering, monitoring Erlang, gestion mémoire).
Redis (via Streams) convient aux architectures qui utilisent déjà Redis pour le cache. Il est simple à opérer mais offre moins de garanties de durabilité : un crash peut perdre des messages si la persistance RDB/AOF n'est pas correctement configurée.
Amazon SQS est le choix naturel sur AWS. Zéro maintenance ops, scaling automatique, dead letter queues natives. Le compromis : un délai de livraison minimum plus élevé et l'absence de routing avancé.
At-least-once, exactly-once et idempotence
Aucun des deux outils ne garantit un traitement exactly-once. Messenger et Enqueue fonctionnent en at-least-once : un message sera traité au moins une fois, potentiellement plus en cas de crash du worker entre le traitement et l'acknowledgement.
C'est une réalité architecturale fondamentale. Chaque handler doit être idempotent : traiter deux fois le même message ne doit pas corrompre l'état du système. Concrètement, cela signifie utiliser des clés d'idempotence (l'ID du rapport dans notre exemple), vérifier l'état avant d'agir, et préférer les opérations UPSERT aux INSERT.
Scaling des consumers et implications CQRS
Scaling horizontal
Pour absorber un pic de charge, on ajoute des workers. Mais multiplier les workers sur un transport Doctrine provoque des contentions. Avec RabbitMQ ou SQS, le scaling horizontal est linéaire : chaque worker supplémentaire consomme sa part de messages sans conflit.
En production, supervisez vos workers avec supervisor ou systemd. Configurez un --memory-limit et un --time-limit pour forcer le recyclage régulier des processus et éviter les fuites mémoire.
Messenger comme fondation CQRS
Messenger ne se limite pas à l'async. En déclarant plusieurs bus (command bus, query bus, event bus), il devient la colonne vertébrale d'une architecture CQRS. Les commands modifient l'état, les queries le lisent, les events propagent les effets de bord. Ce pattern découple votre domaine métier de l'infrastructure et facilite la testabilité.
Enqueue ne propose pas cette abstraction. C'est un outil de queuing, pas un bus applicatif. Si votre ambition est de structurer votre architecture autour du messaging, Messenger est le choix naturel.
Verdict
Choisissez Messenger si vous construisez une application Symfony et que le messaging est un moyen (traitement asynchrone, CQRS, découplage). C'est le standard de l'écosystème, il évolue avec chaque version de Symfony, et sa communauté est massive.
Choisissez Enqueue si vous devez interagir avec un écosystème polyglotte existant, si vous avez besoin d'un transport exotique, ou si le monitoring intégré est un critère décisif.
Dans les deux cas, investissez dans un broker dédié dès que le volume dépasse quelques milliers de messages par jour, rendez vos handlers idempotents, et traitez la sérialisation comme un contrat d'API.
Pour aller plus loin
Bien entendu, si vous souhaitez aller plus loin, nous vous mettons à disposition des développeurs spécialisés sur Symfony qui sauront les intégrer dans vos projets. N'hésitez pas à nous contacter, ou à approfondir la documentation de Messenger et de PHP Enqueue.
- Symfony Messenger : colonne vertébrale de l'architecture hexagonale — aller plus loin avec Messenger
- Les bundles les plus utilisés dans les projets Symfony — l'écosystème Symfony
- Quelle architecture choisir : micro-service ou monolithe modulaire ? — choix d'architecture
- Symfony Messenger — documentation officielle — guide complet du composant
- RabbitMQ — site officiel — broker de messages
- PHP Enqueue — documentation — bibliothèque de queuing PHP