Symfony Messenger : files de messages, workers et architecture asynchrone en 2026
Guide complet Symfony Messenger couvrant l'architecture du bus de messages, la configuration des transports, la gestion des workers, le middleware de deduplication, les strategies de retry et le transport AMQP streaming dans Symfony 7.3+.

Symfony Messenger constitue la brique fondamentale du traitement asynchrone dans l'ecosysteme PHP moderne. Le composant orchestre l'envoi, le routage et la consommation de messages via un bus structure, des transports configurables et des workers supervises. Avec les apports de Symfony 7.3 et la preparation de Symfony 8.0 -- middleware de deduplication, transport AMQP streaming, keepalive Doctrine -- Messenger se positionne desormais face aux systemes de files dediees comme Laravel Horizon ou Sidekiq en termes de couverture fonctionnelle.
Symfony 7.3 introduit le DeduplicateMiddleware pour la deduplication automatique des messages, le keepalive sur le transport Doctrine pour empecher la redistribution des taches longues, et l'attribut #[AsMessage] pour le routage declaratif des transports. Ces trois ajouts transforment la gestion des files de messages en production.
Architecture de Symfony Messenger : bus, transport et worker
Le composant Messenger separe trois responsabilites distinctes : le dispatching (le bus), la livraison (le transport) et le traitement (le worker). Un message est un objet PHP simple. Un handler est une classe invocable. Le bus relie les deux, traverse la pile de middlewares et serialise optionnellement le message vers un transport pour un traitement asynchrone.
namespace App\Message;
final readonly class InvoiceGenerated
{
public function __construct(
public int $orderId,
public string $customerEmail,
) {}
}Ce message ne transporte que des donnees scalaires, pas des entites Doctrine. Passer des identifiants plutot que des objets evite les problemes de serialisation et maintient les messages legers. Le handler recupere les donnees fraiches depuis la base de donnees au moment du traitement.
namespace App\MessageHandler;
use App\Message\InvoiceGenerated;
use App\Service\InvoiceService;
use App\Service\MailerService;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
#[AsMessageHandler]
final readonly class InvoiceGeneratedHandler
{
public function __construct(
private InvoiceService $invoiceService,
private MailerService $mailerService,
) {}
public function __invoke(InvoiceGenerated $message): void
{
$pdf = $this->invoiceService->generatePdf($message->orderId);
$this->mailerService->sendInvoice(
$message->customerEmail,
$pdf,
);
}
}Le dispatch du message depuis un controleur ou un service se resume a une seule ligne :
$this->bus->dispatch(new InvoiceGenerated(
orderId: $order->getId(),
customerEmail: $order->getCustomer()->getEmail(),
));Le bus decide de l'execution synchrone ou asynchrone en fonction de la configuration de routage des transports. Si aucun transport n'est associe au message, le handler s'execute immediatement dans le processus courant. Des qu'un transport est configure, le message est serialise et depose dans la file d'attente correspondante.
Configuration des transports et backends de file
Messenger supporte Doctrine DBAL, Redis, Amazon SQS, Beanstalkd, AMQP (RabbitMQ) ainsi que le nouveau transport AMQP streaming introduit en 2025. Chaque transport se configure via une chaine DSN.
# config/packages/messenger.yaml
framework:
messenger:
failure_transport: failed
transports:
async_priority_high:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
options:
queue_name: high
retry_strategy:
max_retries: 3
delay: 1000
multiplier: 3
max_delay: 60000
async_priority_low:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
options:
queue_name: low
retry_strategy:
max_retries: 5
delay: 5000
multiplier: 2
failed:
dsn: 'doctrine://default?queue_name=failed'
routing:
'App\Message\InvoiceGenerated': async_priority_high
'App\Message\CleanupTempFiles': async_priority_lowLa separation des transports par priorite garantit que les taches sensibles au temps (generation de factures) sont traitees avant les taches de maintenance (nettoyage de fichiers temporaires). Chaque transport dispose de sa propre strategie de retry, ajustee selon la criticite et l'idempotence de ses messages.
Le transport Doctrine ne necessite aucune infrastructure supplementaire mais augmente la charge sur la base de donnees. Redis offre une latence sub-milliseconde. AMQP (RabbitMQ) fournit un routage avance, des dead-letter exchanges et le nouveau transport streaming pour les scenarios a haut debit. Le choix depend de l'infrastructure existante et des exigences de performance.
Gestion des workers avec Supervisor
Les workers consomment les messages depuis les transports. En production, la commande messenger:consume s'execute sous un gestionnaire de processus comme Supervisor ou systemd.
# Consume high-priority messages first, then low-priority
php bin/console messenger:consume async_priority_high async_priority_low \
--memory-limit=128M \
--time-limit=3600 \
--limit=500Les trois flags de limitation empechent les fuites memoire et assurent un redemarrage periodique des workers. --memory-limit arrete le worker lorsqu'il depasse 128 Mo de consommation. --time-limit impose une duree de vie maximale d'une heure. --limit fixe un nombre maximal de messages traites par cycle. Supervisor redemarre le processus apres chaque arret.
; /etc/supervisor/conf.d/messenger-worker.conf
[program:messenger-consume]
command=php /var/www/app/bin/console messenger:consume async_priority_high async_priority_low --memory-limit=128M --time-limit=3600
user=www-data
numprocs=2
process_name=%(program_name)s_%(process_num)02d
autostart=true
autorestart=true
startsecs=0
stopwaitsecs=30
stdout_logfile=/var/log/messenger-worker.log
stderr_logfile=/var/log/messenger-worker-error.logLe parametre numprocs=2 lance deux workers en parallele, doublant le debit de traitement. Ce nombre s'ajuste en fonction des coeurs CPU disponibles et du temps de traitement moyen par message. Pour les taches IO-bound (appels API, envois d'emails), un ratio de 2 a 4 workers par coeur offre un bon equilibre.
Pipeline de middlewares et CQRS avec bus multiples
Les middlewares encapsulent chaque dispatch de message, ajoutant des preoccupations transversales. La pile de middlewares integree gere la validation, les transactions Doctrine et le routage.
# config/packages/messenger.yaml
framework:
messenger:
default_bus: command.bus
buses:
command.bus:
middleware:
- validation
- doctrine_transaction
query.bus:
middleware:
- validation
event.bus:
default_middleware:
allow_no_handlers: true
middleware:
- validationCette configuration implemente le pattern CQRS (Command Query Responsibility Segregation). Les commandes modifient l'etat a l'interieur d'une transaction Doctrine, garantissant la coherence des ecritures. Les queries sont en lecture seule et ne declenchent pas de transaction. Les events autorisent l'absence de handlers via allow_no_handlers: true, activant un pattern pub/sub ou des listeners peuvent etre ajoutes independamment sans modifier le code existant.
La separation en bus distincts apporte un avantage supplementaire lors des entretiens techniques : elle demontre la maitrise de l'architecture hexagonale et de la segregation des responsabilites, deux sujets recurrents dans les postes backend senior.
Prêt à réussir tes entretiens Symfony ?
Entraîne-toi avec nos simulateurs interactifs, fiches express et tests techniques.
Middleware de deduplication dans Symfony 7.3
Les messages dupliques gaspillent des ressources et peuvent provoquer des effets de bord -- double facturation d'un client, envoi multiple du meme email. Symfony 7.3 introduit le DeduplicateMiddleware pour ignorer automatiquement les messages identiques deja presents dans la file.
namespace App\Message;
use Symfony\Component\Messenger\Stamp\DeduplicateStamp;
final readonly class SendWelcomeEmail
{
public function __construct(
public int $userId,
) {}
}// Dispatching with deduplication
use Symfony\Component\Messenger\Stamp\DeduplicateStamp;
$this->bus->dispatch(
new SendWelcomeEmail(userId: 42),
[new DeduplicateStamp(id: 'welcome-email-42')],
);Le DeduplicateStamp prend un identifiant de ressource de verrou. Si un message portant le meme identifiant est deja en attente de traitement, le nouveau dispatch est silencieusement ignore. Ce mecanisme repose sur le composant Lock avec un store serialisable (Redis, Memcached ou base de donnees). En entretien, la question porte souvent sur la distinction entre deduplication a l'emission (ce stamp) et idempotence a la reception (verification dans le handler).
Strategies de retry et transport d'echec
Lorsqu'un handler leve une exception, Messenger relance le message selon la strategie de retry du transport. Une fois les tentatives epuisees, le message est transfere vers le failure transport.
namespace App\MessageHandler;
use App\Message\ProcessPayment;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Symfony\Component\Messenger\Exception\RecoverableMessageHandlingException;
#[AsMessageHandler]
final class PaymentHandler
{
public function __invoke(ProcessPayment $message): void
{
try {
$this->gateway->charge($message->amount, $message->token);
} catch (GatewayTimeoutException $e) {
// Recoverable: retry with backoff
throw new RecoverableMessageHandlingException(
'Payment gateway timeout, retrying',
previous: $e,
);
} catch (InvalidCardException $e) {
// Unrecoverable: send to failure transport immediately
throw new UnrecoverableMessageHandlingException(
'Invalid card, no retry',
previous: $e,
);
}
}
}RecoverableMessageHandlingException declenche la strategie de retry avec backoff exponentiel. UnrecoverableMessageHandlingException contourne completement les retries et envoie le message directement au failure transport. Cette distinction evite de gaspiller des tentatives sur des messages definitivement invalides -- une carte bancaire rejetee ne deviendra pas valide apres trois tentatives supplementaires.
# Inspect and manage failed messages
php bin/console messenger:failed:show
php bin/console messenger:failed:show 20 --transport=failed
# Retry specific messages
php bin/console messenger:failed:retry 20 30
# Filter and remove by class (Symfony 7.3+)
php bin/console messenger:failed:remove --class-filter="App\Message\CleanupTempFiles"La commande messenger:failed:show permet d'inspecter les messages echoues avec leur stack trace et le nombre de tentatives effectuees. La commande messenger:failed:retry permet de rejouer des messages specifiques apres correction du probleme sous-jacent. Le filtre par classe, ajoute dans Symfony 7.3, facilite le nettoyage des messages obsoletes sans affecter les autres.
Les retries impliquent qu'un handler peut s'executer plusieurs fois pour le meme message. Chaque handler doit etre concu de maniere idempotente : verifier si le travail a deja ete effectue avant de le repeter. Les contraintes d'unicite en base de donnees et les flags de statut empechent le double traitement.
Keepalive Doctrine et messages a execution longue
Les handlers de messages a execution longue risquent de voir leurs messages redistribues lorsque le timeout de visibilite du transport expire. Symfony 7.2 a introduit le keepalive pour Redis, SQS et Beanstalkd. Symfony 7.3 etend cette fonctionnalite au transport Doctrine.
# Enable keepalive to prevent redelivery during long processing
php bin/console messenger:consume async --keepaliveLe flag --keepalive met periodiquement a jour le timestamp delivered_at dans la table du transport Doctrine, signalant que le worker traite toujours activement le message. Sans ce flag, un message dont le traitement depasse le timeout du transport (5 minutes par defaut) est recupere par un autre worker, provoquant un traitement en double. Ce scenario est particulierement critique pour les taches de generation de rapports ou d'export de donnees volumineuses.
Attribut #[AsMessage] pour le routage declaratif
Symfony 7.2 a introduit l'attribut #[AsMessage], deplacant le routage des transports du YAML vers la classe de message elle-meme.
namespace App\Message;
use Symfony\Component\Messenger\Attribute\AsMessage;
#[AsMessage(transport: 'async_priority_low')]
final readonly class GenerateReport
{
public function __construct(
public int $reportId,
public string $format = 'pdf',
) {}
}Cette approche elimine la section routing dans messenger.yaml pour ce message. Le transport est declare a la source, rendant le code auto-documente. Les deux approches (routage YAML et attribut) coexistent, l'attribut prenant la precedence. En entretien, cette fonctionnalite illustre la tendance de Symfony vers la configuration declarative via les attributs PHP 8, un mouvement initie avec #[AsController] et #[AsMessageHandler].
Transport AMQP streaming pour les files a haut debit
Le transport AMQP traditionnel utilise le polling (get()) pour recuperer les messages, generant une charge inutile sur RabbitMQ. Le transport AMQP streaming publie en 2025 adopte un modele push (consume()), reduisant la latence et l'utilisation des ressources.
# config/packages/messenger.yaml
framework:
messenger:
transports:
streaming:
dsn: 'amqp-lib://guest:guest@localhost:5672/%2f/messages'
options:
exchange:
name: app_events
type: topic
queues:
order_events:
binding_keys: ['order.*']Les differences cles par rapport au transport AMQP classique : aucune extension C requise (utilise php-amqplib), livraison en streaming via des connexions TCP persistantes, et support natif des topic exchanges avec routage par binding keys. Ce transport gere plusieurs milliers de messages par seconde avec une consommation CPU minimale. Pour les architectures evenementielles a fort volume, le transport streaming associe a des binding keys granulaires permet de distribuer les messages vers des consumers specialises sans multiplier les exchanges.
Conclusion
- Passer des identifiants scalaires dans les messages, pas des entites Doctrine ; recuperer les donnees fraiches dans le handler pour eviter les problemes de serialisation et les etats obsoletes
- Separer les transports par priorite et criticite ; chaque transport dispose de sa propre strategie de retry et de son pool de workers dedie
- Utiliser
RecoverableMessageHandlingExceptionetUnrecoverableMessageHandlingExceptionpour controler explicitement le comportement de retry - Activer
--keepalivesur les workers du transport Doctrine pour empecher la redistribution des messages a execution longue - Appliquer
DeduplicateStampsur les messages ou le traitement en double provoque des effets de bord (paiements, emails, notifications) - Implementer CQRS avec des bus multiples : bus de commandes pour les mutations avec transactions Doctrine, bus de queries pour les lectures, bus d'events pour le pub/sub
- Utiliser Supervisor ou systemd en production avec les flags
--memory-limit,--time-limitet--limitpour la gestion du cycle de vie des workers - Envisager le transport AMQP streaming pour les scenarios a haut debit necessitant une latence sub-milliseconde
Passe à la pratique !
Teste tes connaissances avec nos simulateurs d'entretien et tests techniques.
Tags
Partager
Articles similaires

Doctrine ORM : Maîtriser les relations en Symfony
Guide complet des relations Doctrine ORM dans Symfony. OneToMany, ManyToMany, stratégies de chargement et optimisation des performances avec exemples pratiques.

Questions d'entretien Symfony : Top 25 en 2026
Les 25 questions d'entretien Symfony les plus posées. Architecture, Doctrine ORM, services, sécurité, formulaires et tests avec réponses détaillées et exemples de code.

Symfony 7 : API Platform et bonnes pratiques
Guide complet pour créer des APIs REST professionnelles avec Symfony 7 et API Platform 4. State Providers, Processors, validation et sérialisation expliqués.