emag-tech-labs/messenger-mongo-bundle
A Symfony Messenger transport that stores queued and failed messages in MongoDB. Install via Composer, configure DSN, database, collection, queue, and redeliver timeout, then use standard Messenger retry and failed-message commands.
Install the bundle and make sure the MongoDB extension and library are present:
composer require emag-tech-labs/messenger-mongo-bundle
php -m | grep mongodb # ext-mongodb must be loaded
Check config/bundles.php; if Flex did not register it, add:
EmagTechLabs\MessengerMongoBundle\MessengerMongoBundle::class => ['all' => true],
The first thing to look at is the transport factory and transport classes inside the bundle (Transport/ directory) — that is where the DSN scheme, supported options, and the collection document shape are defined. Then wire a transport:
# config/packages/messenger.yaml
framework:
messenger:
transports:
mongo_async:
dsn: '%env(MESSENGER_MONGO_DSN)%'
options:
database: 'app'
collection: 'messenger_messages'
# .env
MESSENGER_MONGO_DSN=mongodb://mongo:27017
Route a message and consume it:
framework:
messenger:
routing:
'App\Message\SendWelcomeEmail': mongo_async
$this->bus->dispatch(new SendWelcomeEmail($userId));
bin/console messenger:consume mongo_async -vv
First use case to adopt: make it the failure_transport before using it for primary async work. The bundle's sweet spot is persistent failed-message storage in an app that already runs MongoDB:
framework:
messenger:
failure_transport: mongo_failed
transports:
mongo_failed:
dsn: '%env(MESSENGER_MONGO_DSN)%'
options:
database: 'app'
collection: 'messenger_failed'
Then use messenger:failed:show, messenger:failed:retry, and messenger:failed:remove exactly as with Doctrine or AMQP.
Separate collections per transport. Treat one Mongo collection as one queue. It keeps inspection, indexing, and truncation simple, and avoids relying on filtering fields inside a shared collection:
transports:
mongo_async:
dsn: '%env(MESSENGER_MONGO_DSN)%'
options: { database: 'app', collection: 'messenger_async' }
mongo_failed:
dsn: '%env(MESSENGER_MONGO_DSN)%'
options: { database: 'app', collection: 'messenger_failed' }
Use it for low-to-moderate volume async, not bulk pipelines. It behaves like a polling queue rather than a push broker. Good fits: emails, webhooks, report generation, cache warmups, exports. Poor fits: thousands of messages per second, strict FIFO, or latency-sensitive jobs.
Configure retries on the transport, not only in handlers:
mongo_async:
dsn: '%env(MESSENGER_MONGO_DSN)%'
retry_strategy:
max_retries: 3
delay: 1000
multiplier: 2
max_delay: 10000
Design handlers for at-least-once delivery. A consumer crash, timeout, or visibility expiry can cause redelivery. Use idempotency keys, unique constraints, or “already processed” markers:
public function __invoke(SendWelcomeEmail $message): void
{
if ($this->processedLog->exists($message->id)) {
return;
}
// ...
$this->processedLog->mark($message->id);
}
Pass a dedicated MongoDB client for auth, TLS, or replica-set DSNs. If the factory accepts a client option/service, reference a configured MongoDB\Client service rather than embedding credentials and query parameters in the Messenger DSN:
services:
app.mongo_client:
class: MongoDB\Client
arguments: ['%env(MONGODB_URI)%']
transports:
mongo_async:
dsn: '%env(MESSENGER_MONGO_DSN)%'
options:
database: 'app'
collection: 'messenger_async'
client: 'app.mongo_client'
Confirm the exact option name in the bundle factory; this is the main integration point to verify against the installed version.
Worker operations. Run consumers under Supervisor or systemd with bounded lifetimes so PHP memory is recycled:
bin/console messenger:consume mongo_async mongo_failed \
--time-limit=3600 \
--memory-limit=128M \
--sleep=1 \
-vv
Use bin/console messenger:stop-workers as part of deployment so old workers exit cleanly after a code deploy.
Give the application a restricted Mongo user that has readWrite only on the messenger collections, plus createCollection if the bundle auto-creates them. If production users cannot create collections, pre-create them via migration/provisioning and set any auto-setup option off.
Monitor as a collection, not as a queue. Useful day-to-day queries:
// backlog
db.messenger_async.countDocuments({})
// oldest waiting item — confirm the actual field names in the transport source
db.messenger_async.find().sort({created_at: 1}).limit(1)
// failure inspection
db.messenger_failed.find().sort({created_at: -1}).limit(5)
--sleep and worker count deliberately. Empty polling loops are cheap but not free.mongod will not support them. Verify the bundle's claim strategy and use a replica set in production if needed.created_at will delete waiting messages. If you want retention on the failure collection, apply TTL only there and only after deciding how long failures must remain retryable.MongoDB\Client service for complex auth.mongo_failed is also routed into the same failing workflow, messenger:failed:retry can bounce messages straight back. Keep failure transport isolated from normal routing.messenger:consume -vvv, then inspect the raw documents in the collection. Check _id, timestamps, and whether the message was acknowledged/removed. Enable MongoDB driver logging in dev if the transport swallows connection issues.How can I help you explore Laravel packages today?