Choosing a Symfony Messenger Transport: Doctrine, Redis, or RabbitMQ
When adding asynchronous processing to a Symfony 6.4/7.x app, the Messenger transport you pick dictates latency, durability, and operational overhead. This guide compares Doctrine, Redis, and RabbitMQ, then walks through a minimal, verifiable setup for each.
09 Sept 2026, 04:16 UTC

Problem: Picking the Right Messenger Transport
Symfony's Messenger component lets you decouple message dispatch from handling, but the transport you choose determines how messages are stored, routed, and delivered. The three most common transports are:
- Doctrine – uses a database table.
- Redis – uses a Redis stream (or list).
- RabbitMQ – uses an AMQP broker.
Each option has trade‑offs around durability, latency, scalability, and operational complexity. The decision depends on your current stack, expected throughput, and tolerance for infrastructure overhead.
Decision Constraints
- Throughput – How many messages per second must be processed?
- Durability & Ordering – Do you need guaranteed delivery, message ordering, or dead‑lettering?
- Existing Infrastructure – Is a database, Redis, or RabbitMQ already in use?
- Operational Overhead – How much maintenance (cluster, monitoring, backups) are you willing to handle?
- Latency Sensitivity – Are user‑facing requests waiting for background work?
Transport Comparison
| Feature | Doctrine | Redis (stream) | RabbitMQ |
|---|---|---|---|
| Infrastructure | Database only – no extra services. | Redis instance – often already in place. | AMQP broker – Erlang runtime, cluster optional. |
| Latency | High – polling interval (default 1 s). Can be tuned to 100 ms with indexes. | Low – sub‑millisecond pub/sub; streams add a few ms. | Low – broker pushes messages immediately. |
| Throughput | ≈ 100–500 msg/s on a single consumer; limited by DB I/O. | Thousands of msg/s; stream consumer groups scale horizontally. | Unlimited with proper clustering; supports millions of msg/s. |
| Durability | ACID via DB transactions. | Stream persistence configurable; lists are volatile. | Strong persistence; can set TTL, DLX, etc. |
| Ordering | Strict – DB row order. | Stream order preserved per consumer group. | Per‑queue order; routing can change order. |
| Dead‑Letter & Retry | Implemented via Symfony retry_strategy; moves to failure_transport. | Same – but requires Redis streams for persistence. | Built‑in DLX, priority queues, and TTL. |
| Operational Complexity | Low – no extra services. | Medium – need Redis, configure streams. | High – broker cluster, monitoring, Erlang. |
| Polyglot Consumers | Limited – only PHP can read DB rows. | Many languages support Redis streams. | Many languages support AMQP. |
Trade‑Off Summary
- Doctrine is the simplest to start with if you already have a Doctrine setup and your message volume is modest. It's great for transactional outbox patterns and when you need ACID guarantees.
- Redis offers the best latency and horizontal scaling for high‑throughput workloads, but you must enable streams and handle consumer group offsets manually. It's ideal when you already run Redis for caching or session storage.
- RabbitMQ shines in complex routing, guaranteed delivery, and polyglot environments. It requires the most operational effort but gives you the richest feature set (DLX, priorities, headers).
Concrete Implementation & Validation
Below are minimal, verifiable configurations for each transport. All examples assume a Symfony 6.4/7.x LTS project with messenger.yaml in config/packages/ and a message class App\Message\TestMessage with a handler App\MessageHandler\TestMessageHandler that logs the payload.
1. Doctrine Transport
# composer require symfony/messenger doctrine/doctrine-messenger
# src/Message/TestMessage.php
namespace App\Message;
final class TestMessage
{
public function __construct(public string $payload)
{
}
}
# src/MessageHandler/TestMessageHandler.php
namespace App\MessageHandler;
use App\Message\TestMessage;
use Psr\Log\LoggerInterface;
final class TestMessageHandler
{
public function __construct(private LoggerInterface $logger)
{
}
public function __invoke(TestMessage $message): void
{
$this->logger->info('Handled TestMessage', ['payload' => $message->payload]);
}
}
# config/packages/messenger.yaml
framework:
messenger:
transports:
async: doctrine://default?queue_name=async
routing:
'App\\Message\\TestMessage': async
retry_strategy:
max_retries: 3
delay: 1000
multiplier: 2
max_delay: 20000
Dispatch the message from a controller or command:
# src/Controller/TestController.php
namespace App\Controller;
use App\Message\TestMessage;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Messenger\MessageBusInterface;
class TestController extends AbstractController
{
public function index(MessageBusInterface $bus): Response
{
$bus->dispatch(new TestMessage('hello doctrine'));
return new Response('Message queued');
}
}
Consume messages:
php bin/console messenger:consume async --limit=1 -vv
Validate by inspecting the DB table:
SELECT * FROM messenger_messages LIMIT 10;
2. Redis Stream Transport
# composer require symfony/messenger symfony/redis-messenger
# config/packages/messenger.yaml
framework:
messenger:
transports:
async: redis://localhost?stream=1&queue_name=async
routing:
'App\\Message\\TestMessage': async
retry_strategy:
max_retries: 3
delay: 1000
multiplier: 2
max_delay: 20000
Consume:
php bin/console messenger:consume async --limit=1 -vv
Check the stream:
redis-cli XRANGE messages * * COUNT 10
3. RabbitMQ Transport
# composer require symfony/messenger amqp-php-bundle
# config/packages/messenger.yaml
framework:
messenger:
transports:
async: amqp://guest:guest@localhost:5672/%2f?queue=async
routing:
'App\\Message\\TestMessage': async
retry_strategy:
max_retries: 3
delay: 1000
multiplier: 2
max_delay: 20000
Consume:
php bin/console messenger:consume async --limit=1 -vv
Verify via RabbitMQ Management UI (default http://localhost:15672) – open the async queue and confirm the message exists.
Validation Checklist
- Dispatch a
TestMessageand runmessenger:consumewith--limit=1.- Check logs for "Handled TestMessage".
- Inspect the transport store (DB table, Redis stream, RabbitMQ queue).
- Trigger a failure in the handler (throw an exception) and verify the retry stamps increase until the message lands in
failure_transport. - Measure throughput by running
messenger:consume async --memory-limit=256M --time-limit=60and counting processed messages. - For Redis, ensure
stream=1is set; otherwise messages may be lost on consumer restart. - For RabbitMQ, confirm the
amqpPHP extension is installed; otherwise the fallbackphp-amqplibwill be used and performance degraded.
Operational Tips
- Doctrine – Add indexes on
available_atanddelivered_atto improve polling performance. Considerpoll_interval=200inmessenger.yamlfor lower latency. - Redis – Use consumer groups for parallelism:
messenger:consume async --group=web --consumer=web1. MonitorXPENDINGto detect stalled consumers. - RabbitMQ – Use the official
php-amqplib/rabbitmq-bundlefor full feature support. Enablemanagementplugin for queue inspection.
Conclusion
Pick Doctrine for low‑to‑moderate traffic, existing DB, and zero extra infra. Choose Redis when you need sub‑millisecond latency and already run Redis, but be sure to use streams and consumer groups. Opt for RabbitMQ if your workload demands complex routing, guaranteed delivery, or polyglot consumers, and you can afford the operational overhead.
Use the validation checklist to confirm that messages are correctly queued, retried, and processed. Adjust the retry_strategy and transport‑specific options to fine‑tune behaviour for your production environment.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.