# Symfony Messenger: Queues, Workers en Asynchrone Architectuur in 2026 > Diepgaande analyse van Symfony Messenger: message bus-architectuur, transport-configuratie, worker-beheer, deduplicatie-middleware, retry-strategieen, en het streaming AMQP-transport in Symfony 7.3+. - Published: 2026-05-20 - Updated: 2026-05-20 - Author: SharpSkill - Tags: symfony, messenger, php, async, message queue, workers - Reading time: 12 min --- Symfony Messenger verwerkt asynchrone taken in PHP via een gestructureerde message bus, configureerbare transports en beheerde workers. Met Symfony 7.3 en 8.0 die deduplicatie-middleware, streaming AMQP en Doctrine keepalive introduceren, concurreert de component inmiddels met gespecialiseerde queue-systemen zoals Laravel Horizon of Sidekiq op het gebied van functionaliteit en volwassenheid. Dit artikel behandelt de volledige Messenger-stack: van architectuurprincipes en transport-backends tot geavanceerde patronen als CQRS, retry-strategieen en deduplicatie. Elk onderdeel wordt voorzien van productieklare codevoorbeelden en configuraties die direct toepasbaar zijn in technische sollicitatiegesprekken. > **Nieuwe functies in Symfony 7.3+ Messenger** > > Symfony 7.3 voegt DeduplicateMiddleware toe voor automatische berichtdeduplicatie, Doctrine transport keepalive om heraflevering bij langlopende taken te voorkomen, en het #[AsMessage]-attribuut voor declaratieve transport-routing direct vanuit de message-klasse. ## Symfony Messenger-architectuur: Bus, Transport en Worker De Messenger-component scheidt drie verantwoordelijkheden: het versturen (de bus), de bezorging (het transport) en de verwerking (de worker). Een message is een gewoon PHP-object zonder afhankelijkheden van het framework. Een handler is een invokeerbare klasse die precies een berichttype verwerkt. De bus verbindt beide onderdelen, routeert het bericht door een middleware-pipeline en serialiseert het optioneel naar een transport voor asynchrone verwerking. ```php // src/Message/InvoiceGenerated.php namespace App\Message; final readonly class InvoiceGenerated { public function __construct( public int $orderId, public string $customerEmail, ) {} } ``` Dit bericht bevat uitsluitend scalaire data, geen Doctrine-entiteiten. Het doorgeven van ID's in plaats van objecten voorkomt serialisatieproblemen en houdt berichten lichtgewicht. De handler haalt actuele gegevens op uit de database op het moment van verwerking, waardoor verouderde state geen rol speelt. ```php // src/MessageHandler/InvoiceGeneratedHandler.php 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, ); } } ``` Het dispatchen van een message vanuit een controller of service vereist slechts een enkele aanroep: ```php // src/Controller/OrderController.php $this->bus->dispatch(new InvoiceGenerated( orderId: $order->getId(), customerEmail: $order->getCustomer()->getEmail(), )); ``` De bus bepaalt of de uitvoering synchroon dan wel asynchroon verloopt op basis van de transport-routeringsconfiguratie. Zonder geconfigureerde routing wordt het bericht direct in hetzelfde proces afgehandeld. Zodra een transport is toegewezen, serialiseert Messenger het bericht en plaatst het in de bijbehorende queue. ## Transport-configuratie en queue-backends Messenger ondersteunt Doctrine DBAL, Redis, Amazon SQS, Beanstalkd, AMQP (RabbitMQ) en het nieuwe [streaming AMQP-transport](https://symfony.com/blog/introducing-a-streaming-amqp-transport-for-symfony-messenger) dat in 2025 werd geintroduceerd. Elk transport wordt geconfigureerd als een DSN-string in `messenger.yaml`. ```yaml # 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_low ``` Het opsplitsen van transports op basis van prioriteit garandeert dat tijdkritische taken (factuurgeneratie) eerder worden verwerkt dan onderhoudstaken (opruimen van tijdelijke bestanden). Elk transport beschikt over een eigen retry-strategie, nauwkeurig afgestemd op de kriticiteit en het idempotente karakter van de bijbehorende berichten. Het high-priority transport begint met een vertraging van 1 seconde die verdrievoudigt bij elke poging, terwijl het low-priority transport een minder agressief schema hanteert. > **Doctrine vs. Redis vs. AMQP** > > Doctrine transport vereist geen aanvullende infrastructuur, maar verhoogt de databasebelasting. Redis biedt latency onder de milliseconde en is ideaal voor kleinere tot middelgrote volumes. AMQP (RabbitMQ) levert geavanceerde routering, dead-letter exchanges en het nieuwe streaming transport voor scenario's met hoge doorvoer. De keuze hangt af van de bestaande infrastructuur en de vereiste verwerkingscapaciteit. ## Worker-beheer met Supervisor Workers consumeren berichten vanuit transports. In productieomgevingen draait het `messenger:consume`-commando onder een process manager zoals [Supervisor](http://supervisord.org/) of systemd, die het proces bewaakt en automatisch herstart bij onverwachte beeindiging. ```bash # 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=500 ``` De drie limietvlaggen vormen de eerste verdedigingslinie tegen geheugenlekken en procesveroudering. `--memory-limit=128M` stopt de worker zodra het geheugenverbruik de grens overschrijdt. `--time-limit=3600` beperkt de levensduur tot een uur. `--limit=500` stelt een maximum aan het aantal verwerkte berichten per cyclus. Supervisor herstart het proces na elke beeindiging, waardoor een continue verwerkingscyclus ontstaat. ```ini ; /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.log ``` Met `numprocs=2` worden twee parallelle workers gestart, wat de verwerkingscapaciteit verdubbelt. Het optimale aantal hangt af van het beschikbare aantal CPU-cores en de aard van de taken. Bij IO-gebonden verwerking (API-aanroepen, e-mailverzending) levert een verhouding van 2 tot 4 workers per core doorgaans de beste balans tussen doorvoer en resourcegebruik. ## Middleware-pipeline en CQRS met meerdere bussen Middleware omhult elke berichtdispatch en voegt cross-cutting concerns toe aan de verwerkingspipeline. De ingebouwde middleware-stack verzorgt validatie, Doctrine-transacties en routering. ```yaml # 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: - validation ``` Deze configuratie implementeert CQRS (Command Query Responsibility Segregation) via drie gescheiden bussen. Commands muteren applicatiestatus binnen een Doctrine-transactie, wat automatische rollback garandeert bij fouten. Queries zijn strikt read-only en starten bewust geen transactie. Events accepteren nul of meer handlers via `allow_no_handlers: true`, waarmee een pub/sub-patroon wordt gerealiseerd: listeners kunnen onafhankelijk van elkaar worden toegevoegd of verwijderd zonder invloed op bestaande logica. Het vermogen om deze architectuurkeuze te motiveren is een terugkerend thema in senior backend-interviews. Het onderscheid tussen command bus en query bus demonstreert beheersing van het single responsibility principle op message-niveau. ## Deduplicatie-middleware in Symfony 7.3 Dubbele berichten verspillen serverbronnen en veroorzaken potentieel ernstige bijwerkingen: tweemaal factureren, meervoudige e-mailbezorging of onbedoelde dubbele API-aanroepen naar externe systemen. Symfony 7.3 introduceert [DeduplicateMiddleware](https://symfony.com/blog/new-in-symfony-7-3-messenger-improvements) om identieke berichten die al in de queue staan automatisch over te slaan. ```php // src/Message/SendWelcomeEmail.php namespace App\Message; use Symfony\Component\Messenger\Stamp\DeduplicateStamp; final readonly class SendWelcomeEmail { public function __construct( public int $userId, ) {} } ``` ```php // Dispatching with deduplication use Symfony\Component\Messenger\Stamp\DeduplicateStamp; $this->bus->dispatch( new SendWelcomeEmail(userId: 42), [new DeduplicateStamp(id: 'welcome-email-42')], ); ``` De `DeduplicateStamp` ontvangt een lock-resource-identifier. Staat er al een bericht met hetzelfde ID in de wachtrij, dan wordt de nieuwe dispatch stilzwijgend genegeerd. Dit mechanisme vereist de [Lock-component](https://symfony.com/doc/current/lock.html) met een serialiseerbare store (Redis, Memcached of database). Een veelgestelde interviewvraag gaat over het verschil tussen deduplicatie bij verzending (deze stamp) en idempotentie bij verwerking (controle in de handler zelf). Beide complementeren elkaar, maar dienen verschillende doeleinden. ## Retry-strategieen en het failure-transport Wanneer een handler een exception gooit, probeert Messenger het bericht opnieuw te verwerken volgens de retry-strategie van het bijbehorende transport. Na het uitputten van alle toegestane pogingen wordt het bericht verplaatst naar het failure-transport, waar het beschikbaar blijft voor handmatige inspectie en eventuele herverwerking. ```php // src/MessageHandler/PaymentHandler.php 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` activeert de retry-strategie met exponentieel toenemende vertraging. `UnrecoverableMessageHandlingException` omzeilt de volledige retry-keten en stuurt het bericht rechtstreeks naar het failure-transport. Dit onderscheid voorkomt zinloze retries bij permanent ongeldige berichten: een geweigerde creditcard wordt na drie extra pogingen niet alsnog geaccepteerd. ```bash # 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" ``` Het commando `messenger:failed:show` toont mislukte berichten inclusief de volledige stack trace en het aantal uitgevoerde pogingen. Met `messenger:failed:retry` kunnen specifieke berichten opnieuw worden ingevoerd nadat het onderliggende probleem is opgelost. Het klassefilter, nieuw in Symfony 7.3, maakt selectief opschonen van verouderde berichten mogelijk zonder andere berichttypen te raken. > **Idempotente handlers zijn essentieel** > > Retries impliceren dat een handler meerdere keren wordt uitgevoerd voor hetzelfde bericht. Ontwerp elke handler als idempotent: controleer altijd of de bewerking al is uitgevoerd voordat deze wordt herhaald. Gebruik database unique constraints of statusvlaggen om dubbele verwerking structureel te voorkomen. ## Doctrine Keepalive bij langlopende berichten Handlers voor langlopende taken lopen het risico dat hun berichten opnieuw worden afgeleverd zodra de visibility timeout van het transport verstrijkt. Symfony 7.2 introduceerde keepalive voor Redis, SQS en Beanstalkd. Symfony 7.3 [breidt deze functionaliteit uit naar het Doctrine-transport](https://symfony.com/blog/new-in-symfony-7-3-messenger-improvements), wat cruciaal is voor teams die zonder externe queue-infrastructuur werken. ```bash # Enable keepalive to prevent redelivery during long processing php bin/console messenger:consume async --keepalive ``` De `--keepalive`-vlag werkt periodiek de `delivered_at`-timestamp bij in de Doctrine-transporttabel, waarmee wordt gesignaleerd dat de worker het bericht nog actief verwerkt. Zonder deze vlag wordt een bericht waarvan de verwerking langer duurt dan de transport-timeout (standaard 5 minuten) opgepakt door een andere worker, met dubbele verwerking als gevolg. Dit scenario doet zich met name voor bij taken als rapportgeneratie, bulkexport of zware berekeningen. ## Het #[AsMessage]-attribuut voor declaratieve routing Symfony 7.2 introduceerde het `#[AsMessage]`-attribuut, waarmee transport-routing verhuist van YAML-configuratie naar de message-klasse zelf. Dit past binnen de bredere Symfony-trend richting PHP 8-attributen als primair configuratiemechanisme. ```php // src/Message/GenerateReport.php 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', ) {} } ``` Deze benadering elimineert de `routing`-sectie in `messenger.yaml` voor het betreffende bericht. Het transport wordt bij de bron gedeclareerd, waardoor de codebase zelfdocumenterend wordt. Beide benaderingen (YAML-routing en attribuut) bestaan naast elkaar, waarbij het attribuut voorrang heeft bij conflicten. In de praktijk kiezen veel teams voor een hybride strategie: het attribuut voor standaardroutering en YAML voor omgevingsspecifieke overschrijvingen. ## Streaming AMQP-transport voor high-throughput queues Het traditionele AMQP-transport maakt gebruik van polling (`get()`) om berichten op te halen, wat onnodige belasting op RabbitMQ genereert bij lage berichtvolumes en onnodige latency bij hoge volumes. Het [streaming AMQP-transport](https://symfony.com/blog/introducing-a-streaming-amqp-transport-for-symfony-messenger) dat in 2025 werd uitgebracht, schakelt over naar een push-model (`consume()`), waardoor zowel latency als CPU-verbruik significant dalen. ```yaml # 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.*'] ``` De voornaamste verschillen ten opzichte van het standaard AMQP-transport: geen C-extensie vereist (het maakt gebruik van [php-amqplib](https://github.com/php-amqplib/php-amqplib)), streaming delivery via langlevende TCP-verbindingen, en native ondersteuning voor topic exchanges met binding key-routing. Dit transport verwerkt duizenden berichten per seconde met minimale CPU-overhead. Voor event-driven architecturen maakt de combinatie van het streaming transport met granulaire binding keys het mogelijk om berichten te distribueren naar gespecialiseerde consumers zonder het aantal exchanges te vermenigvuldigen. ## Conclusie - Geef scalaire ID's door in berichten, geen Doctrine-entiteiten; haal actuele gegevens op in de handler om serialisatieproblemen en verouderde state te voorkomen - Splits transports op basis van prioriteit en kriticiteit; elk transport krijgt een eigen retry-strategie en een dedicated worker-pool - Gebruik `RecoverableMessageHandlingException` en `UnrecoverableMessageHandlingException` om retry-gedrag expliciet aan te sturen - Schakel `--keepalive` in bij Doctrine transport workers om heraflevering van langlopende berichten te voorkomen - Pas `DeduplicateStamp` toe op berichten waarbij dubbele verwerking bijwerkingen veroorzaakt (betalingen, e-mails, notificaties) - Implementeer CQRS met meerdere bussen: command bus voor mutaties met Doctrine-transacties, query bus voor leesoperaties, event bus voor pub/sub - Gebruik Supervisor of systemd in productie met de vlaggen `--memory-limit`, `--time-limit` en `--limit` voor het beheer van de worker-levenscyclus - Overweeg het streaming AMQP-transport voor high-throughput scenario's die latency onder de milliseconde vereisen --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/nl/blog/symfony/symfony-messenger-queues-workers-async-architecture