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.