A Symfony e-commerce app sends the PDF invoice via Messenger as soon as an order is paid. AMQP is configured, workers are running — except the PDF service returns 503 for twenty minutes. Automatic retries redeliver the same message forty times. Result: forty emails to the customer and a saturated queue hiding real incidents.
Messenger is not a magic bus. It is a contract between dispatch time and handler execution — with transport choice, retry policy, middleware, and failure transport. Each option changes resilience and duplication risk.
Transports: sync, Doctrine, Redis, AMQP
Symfony routes messages through transports in messenger.yaml. Routing (App\Message\* -> async) decides where each class goes.
| Transport | Use case | Watch out for |
|---|---|---|
| sync | Dev, ultra-fast handlers | Hides latency and timeouts in prod |
| doctrine://default | Small volume, no Redis | messenger_messages table, DB locks |
| redis:// | Light queues, low ops overhead | Redis persistence, message TTL |
| amqp:// (RabbitMQ) | Prod multi-worker, DLX | Document exchange/queue topology |
On a single VPS, Doctrine may suffice for hundreds of messages per hour. Once handlers call external APIs or run longer than a few seconds, a dedicated broker keeps web requests responsive and simplifies horizontal scaling of messenger:consume workers.
Picking a transport means picking who carries the delivery guarantee — not just where JSON is stored.
Retries, delays, and transient vs permanent errors
RetryStrategy (max_retries, delay, multiplier) redelivers failed messages. Without separating "API down" from "invalid payload," you amplify a code bug.
Apply few retries (3–5) with exponential backoff on network errors. Throw UnrecoverableMessageHandlingException for definitive business errors. Set handler timeouts — a handler waiting forever on a third party blocks an entire worker.
Test in staging: kill the target service and watch queue behavior — not just handler logs.
Failure transport and controlled replay
After retries are exhausted, Messenger can route to a failure transport (failed://, dedicated queue, or Doctrine table). That is your quarantine zone.
Alert when the failure transport is not empty. Use messenger:failed:show and messenger:failed:retry after fix. Define a retention policy — do not keep six months of dead messages unexamined.
Without failure transport, messages may be dropped or spin a worker in a loop — two different ways to lose visibility.
Workers, supervision, and deploy consistency
Workers messenger:consume async -vv --time-limit=3600 --memory-limit=128M are production services. After deploy, restart workers (signal or --stop-when-empty then relaunch). Ensure same code version and env vars as PHP-FPM. Set memory and time limits to catch slow leaks.
On Kubernetes, a separate Deployment for consumers avoids scaling web pods when only the queue grows. Queue-length autoscaling (KEDA, Prometheus) scales the right component — not HTTP frontends.
Middleware, routing, and forgotten sync handlers
Verify every critical business message uses async. A heavy handler left on sync causes nginx timeouts and intermittent 502s that are hard to reproduce.
DoctrinePingConnectionMiddleware and DoctrineCloseConnectionMiddleware prevent stale MySQL connections on long-lived workers — a classic on shared hosting or managed databases with aggressive timeouts.
The peak: transport does not guarantee business semantics
So the first question is not "Redis or RabbitMQ?" It is: what happens if this handler runs twice? Until that answer is clear, transport is just infrastructure detail.
Decide and move forward without blind spots
List your messages with acceptable latency, criticality, and idempotence. Pick transport and failure transport accordingly. Configure backoff retries, supervise workers, and test downstream failure on staging before production.
To size Redis, RabbitMQ, or workers on European VPS and cloud offers, see our directory and compare tool. Guides round out PHP infrastructure framing. Useful exercise: simulate a permanent exception — the message should land in the failure transport without blocking the whole queue.
Frequently asked questions
What is the difference between sync and async in Messenger?
The sync transport runs the handler immediately in the HTTP request — fine in dev, risky in prod for slow tasks. Async sends the message to a queue consumed by separate workers.
Doctrine transport or Redis/AMQP?
Doctrine fits small projects without a dedicated broker, but the messages table becomes a bottleneck. Redis or AMQP scales better once volume, latency, or parallelism grows.
What is the failure transport for?
It isolates messages that failed after retries are exhausted, for inspection and manual replay. Without a configured failure transport, messages may disappear or block consumption.
How do I avoid duplicates with retries?
Make handlers idempotent (lock, DB status) and limit retries on non-transient errors. Use unique business keys — do not assume exactly-once delivery by default.
Before tuning the broker, ask one simple question: if this message is handled twice, does your business survive?