Message queues
Building a report, processing an image, a mailing take seconds or minutes. A visitor must not be kept waiting. The Messenger component postpones such work: the controller puts a message into a queue and answers at once, and a separate process does the job in the background.
A message
A message is a class with the data the job needs:
// src/Message/GenerateReport.php
namespace App\Message;
class GenerateReport
{
public function __construct(
public readonly int $userId,
public readonly string $period,
) {}
}A handler
// src/MessageHandler/GenerateReportHandler.php
namespace App\MessageHandler;
use App\Message\GenerateReport;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
#[AsMessageHandler]
class GenerateReportHandler
{
public function __construct(private UserRepository $users) {}
public function __invoke(GenerateReport $message): void
{
$user = $this->users->find($message->userId);
// the long job: build the report and send it
}
}Symfony works out which message to handle from the argument type.
Dispatching
use Symfony\Component\Messenger\MessageBusInterface;
#[Route('/report', name: 'report', methods: ['POST'])]
public function report(MessageBusInterface $bus): Response
{
$bus->dispatch(new GenerateReport($this->getUser()->getId(), '2026-09'));
$this->addFlash('success', 'The report is being prepared and will arrive by email');
return $this->redirectToRoute('profile');
}Without extra configuration the handler runs at once, in the same request.
An asynchronous transport
A transport is where the queue is stored. The easiest start is the database:
MESSENGER_TRANSPORT_DSN=doctrine://default# config/packages/messenger.yaml
framework:
messenger:
transports:
async: '%env(MESSENGER_TRANSPORT_DSN)%'
routing:
App\Message\GenerateReport: asyncThe routing section says which messages go to the queue. Under heavy load Redis or RabbitMQ replaces the database — only the line in .env changes.
Running a worker
php bin/console messenger:consume async -vvThe command runs continuously and handles messages as they appear. On a server it is run under Supervisor or systemd so that the process restarts after a failure.
A delay
use Symfony\Component\Messenger\Stamp\DelayStamp;
$bus->dispatch(new SendReminder($orderId), [new DelayStamp(60 * 60 * 1000)]);The time is in milliseconds: this message is handled in an hour.
Failed messages
If a handler throws an exception, Messenger retries — three times by default, with a growing pause. After that the message goes to a separate queue:
php bin/console messenger:failed:show
php bin/console messenger:failed:retryCommon mistake
A worker is a long-lived process: it loaded the code once and does not see new files. After a site update it keeps running the old version. On every deployment run php bin/console messenger:stop-workers — the processes finish after the current message, and Supervisor starts them again. And pass an identifier in a message rather than an entity: by the time it is handled, the data in the database may have changed.