Configure Symfony Messenger with Doctrine Transport for Asynchronous Notification
Step‑by‑step guide to configure Symfony Messenger with the Doctrine DBAL transport, create a message and handler, dispatch it, run a worker, and verify processing or handle failures.
07 Sept 2026, 09:04 UTC

Desired outcome
Set up Symfony Messenger to send a notification message to a background worker using the Doctrine DBAL transport, verify that the worker processes the message, and know how to inspect failures.
Prerequisites
- A Symfony 6.2+ project with Flex enabled.
- Access to a database configured in
.env(e.g., MySQL, PostgreSQL). - Composer and PHP CLI available.
Step 1: Install the Messenger component
Run this command in the project root:
composer require symfony/messenger
Flex will create config/packages/messenger.yaml with a default async transport placeholder.
Step 2: Configure the Doctrine transport
Edit .env (do not commit this file) and add a DSN for the async transport:
# .env
MESSENGER_TRANSPORT_DSN=doctrine://default?queue_name=async
Then adjust config/packages/messenger.yaml to use the env var:
# config/packages/messenger.yaml
framework:
messenger:
transports:
async: '%env(MESSENGER_TRANSPORT_DSN)%'
routing:
'App\Message\SendNotification': async
# Optional: failure handling
failure_transport: failed
redelivery:
max_retries: 3
delay: 1000
# Define the failed transport (same DSN, different queue)
transports:
failed: '%env(MESSENGER_TRANSPORT_DSN)%&queue_name=failed'
Step 3: Create a plain PHP message class
Create src/Message/SendNotification.php:
<?php
namespace App\Message;
class SendNotification
{
public function __construct(
private string $recipient,
private string $subject,
private string $body,
) {}
public function getRecipient(): string { return $this->recipient; }
public function getSubject(): string { return $this->subject; }
public function getBody(): string { return $this->body; }
}
The message holds only serializable data; no Doctrine entities or services are included.
Step 4: Create the message handler
Create src/MessageHandler/SendNotificationHandler.php:
<?php
namespace App\MessageHandler;
use App\Message\SendNotification;
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
class SendNotificationHandler implements MessageHandlerInterface
{
public function __invoke(SendNotification $message): void
{
// Replace with real notification logic (email, SMS, etc.)
error_log(sprintf(
'Notification to %s: %s – %s',
$message->getRecipient(),
$message->getSubject(),
$message->getBody()
));
}
}
Step 5: Dispatch the message from a controller or service
Inject MessageBusInterface and dispatch:
use App\Message\SendNotification;
use Symfony\Component\Messenger\MessageBusInterface;
class NotificationController
{
public function __construct(private MessageBusInterface $bus) {}
public function send(): void
{
$this->bus->dispatch(new SendNotification(
'user@example.com',
'Welcome',
'Thanks for signing up!'
));
}
}
Step 6: Start the worker to consume messages
Run the consumer command in a terminal:
php bin/console messenger:consume async -vv
The worker will pull rows from the messenger_messages table, deserialize the message, and invoke the handler.
Expected checks
- After dispatching, look in
var/log/dev.logfor a line like[INFO] Notification to user@example.com: Welcome – Thanks for signing up! - Inspect the transport table:
SELECT * FROM messenger_messages WHERE queue_name = 'async';– a row appears on dispatch and disappears after successful handling. - In the dev environment, open the Symfony profiler for the request that dispatched the message; you should see a
BusNameStampand aHandledStamp.
Recovery options and failure handling
If the handler throws an exception:
- The message is redelivered up to
max_retries(3 by default). - After exhausting retries, it moves to the
failedtransport. - Inspect failed messages with:
php bin/console messenger:failed:show
To retry a specific failed message:
php bin/console messenger:failed:retry <id>
Or delete it permanently:
php bin/console messenger:failed:remove <id>
Limitations and practical verification
The Doctrine transport polls the database; under high volume this can cause noticeable load. For production‑scale workloads consider Redis or RabbitMQ transports.
To verify that the worker is actually processing messages, you can:
- Temporarily add a
sleep(5)inside the handler. - Dispatch a message and observe that the worker command does not return immediately (it will stay busy for ~5 seconds).
- Remove the sleep after verification.
Never commit .env containing database credentials; use Symfony secrets or a vault for production.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.