Symfony Messenger is deceptively simple to get started with. Dispatch a message, write a handler, done. But production has a way of exposing the gaps in your understanding.
Transports and Serialization
The default PHP serializer works fine in development. In production, you want JSON serialization — especially if consumers might be deployed independently from producers.
framework:
messenger:
serializer:
default_serializer: messenger.transport.symfony_serializer
symfony_serializer:
format: json
transports:
async:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
serializer: messenger.transport.symfony_serializer
This matters the moment you deploy a new version of your app while old messages are still sitting in the queue.
Retry Strategy
The default retry strategy is generous — three retries with exponential backoff. That's fine for transient failures but dangerous for poison messages that will never succeed.
transports:
async:
dsn: '%env(MESSENGER_TRANSPORT_DSN)%'
retry_strategy:
max_retries: 3
delay: 1000
multiplier: 3
max_delay: 60000
failure_transport: failed
Always configure a failure transport. Messages that exhaust their retries end up there instead of being silently dropped.
Handling Failures Gracefully
The messenger:failed:show and messenger:failed:retry commands are essential. Build them into your ops playbook from day one.
# See what's in the dead letter queue
php bin/console messenger:failed:show
# Retry a specific message
php bin/console messenger:failed:retry 42
# Retry everything
php bin/console messenger:failed:retry
Middleware Is Your Friend
Need to wrap every handler in a database transaction? Add logging context? Messenger middleware is the right place for cross-cutting concerns — not base handler classes.
final class TransactionMiddleware implements MiddlewareInterface
{
public function __construct(private EntityManagerInterface $em) {}
public function handle(Envelope $envelope, StackInterface $stack): Envelope
{
$this->em->beginTransaction();
try {
$envelope = $stack->next()->handle($envelope, $stack);
$this->em->flush();
$this->em->commit();
return $envelope;
} catch (\Throwable $e) {
$this->em->rollback();
throw $e;
}
}
}
The Takeaway
Messenger is production-ready, but you have to configure it like a production system. Default settings are for getting started, not for staying up at 3 AM.