Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

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: async

The 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 -vv

The 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:retry

Common 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.

Thanks for reading!

Donate to the site — we work hard for you.