--concurrency est une option de messenger:consume introduite par Symfony 8.2 : un seul worker traite plusieurs messages en parallèle, via un pool de processus enfants amphp/parallel. Présentée le 25 septembre 2026 sur le blog officiel avec deux autres nouveautés (le prefetch AMQP et un middleware de logging), elle arrive avec Symfony 8.2, attendu en novembre 2026.
Un worker Messenger traite aujourd'hui un message à la fois. Pendant que ton handler attend une réponse HTTP ou une requête SQL, il ne fait rien d'autre. La seule parade consistait à lancer plusieurs processus messenger:consume et à les superviser, chacun avec sa connexion au transport et son propre boot du kernel. Les chiffres annoncés pour 8.2 sont flatteurs : 7,5 fois plus vite avec 8 messages en parallèle, 13 à 18 fois avec le prefetch. Ils viennent de benchmarks maison, et il faut les lire avec leur contexte.
C'est la deuxième nouveauté Messenger de la série, après l'outbox et le claim check, qui traitaient de fiabilité. Celle-ci traite de débit.
Comment traiter plusieurs messages en parallèle avec --concurrency
Contribution de Nicolas Grekas (PR #63650). L’option demande une dépendance, à installer une fois dans ton projet :
# first, install this dependency once in your project
$ composer require amphp/parallel
# handle up to 4 messages at the same time
$ php bin/console messenger:consume async --concurrency=4Le worker principal continue de récupérer les messages, puis les distribue à un pool de processus enfants qui bootent ton application et exécutent les handlers. Si l'extension PHP parallel est installée, ce sont des threads à la place ; elle est facultative. Conséquence pratique : une seule connexion au transport, quel que soit le niveau de parallélisme. Selon la PR, le worker met la consommation en pause quand tous les enfants sont occupés, transmet --memory-limit aux enfants, et lève une erreur explicite si amphp/parallel manque.
Le benchmark 7,5× et ce qu’il ne dit pas
Le billet annonce que --concurrency=8 est 7,5 fois plus rapide qu'un worker unique sur des messages de 20 ms. La PR précise le protocole : 192 messages de 20 ms, une machine à 24 cœurs, des workers déjà chauds. Le gain est quasi linéaire sur un handler simple (1,89× à 2, 3,73× à 4, 7,52× à 8). C'est un banc synthétique, pas un résultat sur ta charge.
La PR reconnaît une limite : comme il n'y a qu'une connexion, la récupération et la désérialisation des messages ne sont pas parallélisées. C'est défavorable aux tâches très courtes, et favorable aux tâches longues, surtout si le nombre de connexions au broker te coûte. Mon analyse, non mesurée : le gain principal se verra sur les handlers qui attendent des I/O ; pour du calcul pur, le plafond reste le nombre de cœurs.
Le piège des batch handlers
Les batch handlers sont l'exception documentée : tous les messages d'un lot doivent passer par le même processus enfant, donc --concurrency ne les accélère pas. Selon la PR, les messages d'une même classe sont épinglés au même enfant ; avec une seule classe de message batchée, le benchmark donne 0,98× (aucun gain), avec deux classes environ 2×. La recommandation du billet est nette : si tu n'avais choisi un batch handler que pour aller plus vite, et non parce que traiter les messages ensemble coûte moins cher (un INSERT multi-lignes, un appel d'API bulk), remplace-le par un handler classique et --concurrency. Sinon, garde plusieurs workers.
Quand --concurrency remplace-t-il plusieurs workers supervisés ?
Les deux modèles coexistent. Ce qui suit est mon analyse, pas une consigne du billet :
- Handlers bloqués sur des I/O (HTTP, SQL) : bon candidat, un seul processus à superviser, une seule connexion au broker.
- Connexions à la base : chaque enfant boote l’application et a son propre conteneur. Tu réduis les connexions au broker, pas celles à la base ; dimensionne ton pool SQL en conséquence.
- Mémoire :
--concurrency=8boote huit kernels. Mesure la RAM par enfant avant de monter. - Supervision : un seul processus Supervisor ou systemd, mais N enfants. Revois tes healthchecks et tes métriques par processus.
Si tu n'as pas encore industrialisé la supervision et le retry, notre guide pour configurer les workers, le retry et le monitoring de Messenger en production reste la base, avec ou sans --concurrency.
Comment accélérer le transport AMQP avec prefetch_count
Aujourd'hui, le transport AMQP demande un message au broker à la fois, soit un aller-retour réseau par message ; RabbitMQ considère que c'est la manière la moins efficace de consommer. Symfony 8.2 ajoute l'option prefetch_count (PR #65924 et #65168), qui enregistre un vrai consumer sur chaque file et laisse le broker pousser les messages à l'avance :
# .env
MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages?prefetch_count=20Votre dette technique s'accumule ?
Audit complet en 10 jours. Recommandations priorisées et actionnables.
Le billet annonce 13 à 18 fois plus rapide, mesuré contre un broker local. La PR détaille : 5 000 messages acquittés un par un passent de 1 639 à 29 803 messages par seconde dans le meilleur run. C'est le transport seul, sans traitement de handler, sur un broker en loopback : sur un broker distant, l'écart dépendra de ta latence. Point contre-intuitif relevé par la PR : prefetch_count=1 est légèrement plus lent que l'ancien mode. C'est le prefetch qui apporte le gain, d'où la règle du billet : fixe prefetch_count au-dessus de la taille de fetch du worker, qui vaut par défaut la valeur de --concurrency.
L’option est désactivée par défaut, pour deux raisons :
- L’ordre de consommation entre plusieurs files est décidé par le broker, plus par le transport. Si tu comptes sur des files prioritaires, teste avant d’activer.
- Les messages préchargés mais pas encore traités sont redélivrés à l’arrêt du worker. Rien n’est perdu, mais tes handlers doivent supporter d’être rejoués.
Pourquoi les retries AMQP créent tant de files de délai
AMQP n'a pas de délai natif : Symfony crée une file de délai par valeur distincte. Or la stratégie de retry par défaut ajoute du jitter, ce qui produit plus d'un millier de valeurs différentes. Presque chaque retry déclare une nouvelle file. Symfony 8.2 arrondit les délais AMQP à deux chiffres significatifs (5 234 ms deviennent 5 300 ms), sans jamais libérer un message trop tôt et avec au plus 10 % de retard. Mesure de la PR #65168 : pour une stratégie à 60 s avec jitter, on passe de 39 496 files de délai à 23. L'option delay[granularity] fixe un arrondi en millisecondes, et la valeur 1 le désactive :
# .env
MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages?delay[granularity]=1000Attention : c'est le seul changement actif sans option. Si un test ou une alerte dépend du délai exact d'un retry, attends-toi à voir 5 300 ms au lieu de 5 234 ms.
Comment repérer les messages lents avec le middleware logging
Le troisième apport ne rend aucun worker plus rapide : il sert à trouver quels messages sont lents. Contribution de Marie Charles (PR #64621), le middleware logging s'active sur chaque bus à surveiller :
# config/packages/messenger.yaml
framework:
messenger:
buses:
messenger.bus.default:
middleware:
- loggingIl mesure tout ce qui s'exécute après lui dans la pile, et écrit dans le canal messenger avec les clés de contexte class, duration_ms et memory_usage (en octets). Exemple de sortie du billet :
messenger.INFO: "App\Message\GenerateInvoice" message successfully handled. {"class":"App\\Message\\GenerateInvoice","duration_ms":1834,"memory_usage":12582912}
messenger.ERROR: Unable to handle "App\Message\SyncCatalog" message. {"class":"App\\Message\\SyncCatalog","duration_ms":30012,"memory_usage":524288,"exception":"..."}Deux précisions de la PR pour tes dashboards. Un message asynchrone est loggé deux fois dans sa vie : à l'envoi (durée de remise au transport) et dans le worker (durée de traitement). Filtre sur le texte du message, sinon tu compteras chaque message deux fois. Et memory_usage est un delta, pas un pic : il peut même être négatif. Si tu utilisais déjà un middleware maison pour ça, notre article sur l'observabilité de Messenger avec LoggableMiddleware montre comment basculer.
Que faut-il faire pour migrer ?
- Active d’abord le middleware
loggingsur tes bus, et relève lesduration_mspar classe de message. - Installe
amphp/parallelet teste--concurrencysur une file de préproduction, avec tes vrais handlers, avant de retirer le moindre processus de ta supervision. - Remplace les batch handlers qui n’existaient que pour la vitesse.
- Active
prefetch_counten staging, uniquement si tes handlers sont idempotents. - Vérifie qu’aucun retry ne dépend d’un délai exact à la milliseconde.
Ma lecture
Le logging est le premier changement à adopter, le jour de la montée de version : il ne coûte rien et te dit si tes workers attendent des I/O ou calculent. --concurrency vient ensuite, pour les handlers longs ou bloqués sur des I/O, hors batch : le 7,5× vient de 24 cœurs et de messages de 20 ms, pas de ton application. Je laisserais prefetch_count en dernier, car la redélivrance à l'arrêt est un vrai changement de contrat.
Avant d'activer quoi que ce soit, le plus rentable est souvent de savoir ce que font vraiment tes handlers : lesquels sont lents, lesquels ne supportent pas d'être rejoués, lesquels n'ont aucune raison d'être en batch. C'est un travail de lecture de code et de mesure, pas de configuration. Si tu veux un regard extérieur avant de toucher à tes workers, c'est ce que livre un Bear Scan : un audit de 10 jours, un état des lieux et des recommandations classées par priorité.

Écrit par
Flavien Métivier
Expert PHP/Symfony indépendant depuis 2014, ancien CTO de Kartable et d'Extrabat. Plus de 80 projets menés, de l'audit à la migration.
Mon parcours