handcraftedinthealps/redis-transport-bundle
A Symfony bundle that adds a Redis-based transport, letting you route messages through Redis instead of a traditional queue backend. It wires up the transport configuration and supporting services, making it straightforward to plug Redis into a messenger-style workflow with minimal setup in your app...
Install and register. The bundle is a standard Symfony bundle that plugs a Redis Streams TransportInterface implementation into framework.messenger, so nothing else in your Messenger setup has to change — handlers, middleware, retry strategy, and failure transports all keep working.
composer require handcraftedinthealps/redis-transport-bundle
As of 1.2.2 the package supports Symfony 5.4, 6.x and 7.x; Symfony 7 support was the only change in that release (#23), so if you are on a Symfony 7 app, 1.2.2 is your minimum version and 1.2.1 and below will fail dependency resolution. There are no API or config changes between 1.2.1 and 1.2.2 — it is a pure compatibility bump, safe to take on any supported version.
Flex usually registers it in config/bundles.php; if not, add HandcraftedInTheAlps\RedisTransportBundle\RedisTransportBundle::class => ['all' => true].
Look here first. The bundle's job is narrow, so the two things worth reading before you write config are (a) the README's DSN scheme and supported options keys, and (b) the TransportFactory class, which is the single source of truth for what supports() accepts and how options are mapped onto the connection. The factory tells you more in 60 lines than the docs do.
Minimal working config. Because the bundle registers its own transport factory, you reference it by DSN under a normal transport name:
# config/packages/messenger.yaml
framework:
messenger:
transports:
async:
dsn: '%env(MESSENGER_REDIS_DSN)%' # e.g. redis-stream://redis:6379/messages
options:
consumer: '%env(HOSTNAME)%'
routing:
'App\Message\SendInvoice': async
Verify the exact scheme against the README — it must be distinct enough that the bundle's factory claims it before Symfony's built-in Redis transport does. Then create the consumer group if the transport implements SetupableTransportInterface:
php bin/console messenger:setup-transports
php bin/console messenger:consume async -vv
First use case. The lowest-friction win is replacing a Doctrine or AMQP transport for fire-and-forget domain events on infrastructure you already run: you get a durable-ish queue, consumer groups, and no extra broker to operate. If you already depend on symfony/redis-messenger and it meets your needs, this bundle's value is only in whatever it does differently — check that before migrating.
One stream per bounded context, not per message type. Messenger routing fans out from message class to transport, but Redis Streams scale best when a single consumer group owns a single stream. Group your messages into commands, events, low_priority, and let routing do the mapping. This keeps XPENDING and lag monitoring meaningful.
Fan-out via separate consumer groups. The genuinely nice property of Redis Streams is that multiple consumer groups on the same stream each see every message. If two services must both react to an event, point both applications at the same stream with different group names and no shared queue plumbing is needed:
# service A
transports:
billing_events: { dsn: 'redis-stream://redis:6379/events', options: { group: 'billing', consumer: '%env(HOSTNAME)%' } }
# service B
transports:
search_events: { dsn: 'redis-stream://redis:6379/events', options: { group: 'search', consumer: '%env(HOSTNAME)%' } }
Consumer names must be unique per worker process. Use %env(HOSTNAME)% plus the PID or a container ordinal. Two workers sharing a consumer name share the pending-entries list, and a crash then strands messages under an identity that no surviving process owns.
Supervise workers with bounded lifetimes. Blocking XREADGROUP workers hold connections and slowly accumulate memory. Run them with --time-limit=3600 --memory-limit=128M under systemd or Supervisor so they recycle, and make sure stopwaitsecs exceeds the block timeout so SIGTERM is handled gracefully rather than leaving entries in the PEL.
Treat delivery as at-least-once. Handlers should be idempotent or guarded by a dedupe key; Messenger's RedeliveryStamp gives you the attempt count, but Redis itself guarantees nothing beyond redelivery. Combine with a failure transport (failure_transport: failed) so poison messages land somewhere inspectable instead of looping.
Producer-only processes need no consumer. A web request that dispatches only needs the transport configured; it will XADD and return. Don't force every app to run a worker.
Test on the sync transport. In config/packages/test/messenger.yaml, route everything to sync://. Keep one integration test that dispatches through the real transport against a disposable Redis and asserts the handler ran, so serialization regressions surface in CI.
Operational monitoring. XLEN <stream> for backlog, XPENDING <stream> <group> for unacked depth, XINFO GROUPS for last-delivered-id lag. Alert on pending count growth — it's the earliest signal that workers are dying mid-handler.
maxmemory-policy will silently eat your queue. If Redis runs with allkeys-lru or volatile-*, the stream key can be evicted under pressure. Use noeviction and a dedicated Redis instance (or logical DB) rather than sharing with your cache.
The pending-entries list is the sharp edge. A message is delivered, added to the PEL, and only removed on ack(). A worker killed between get() and ack() leaves it pending forever unless something reclaims it via XAUTOCLAIM/XCLAIM. Check whether this bundle's transport implements a redelivery timeout option; if it doesn't, you need an explicit reclaim job or you accept that crashed messages stall. This is the single most important thing to validate before production.
Trimming is not free. If the transport exposes a MAXLEN-style option, understand that aggressive trimming discards entries that consumers have not yet read — losing messages, not just trimming history. Set the cap well above peak backlog and use approximate trimming (~) to avoid per-XADD cost.
No native delayed messages. Redis Streams have no delay primitive. Delayed retries are implemented by re-publishing with a delay mechanism, or not at all. If your workflow needs DelayStamp with precise timing, verify support explicitly; otherwise use a scheduler or a separate delay transport.
reject() semantics differ from AMQP. There is no broker-side nack with requeue. Rejection typically means XACK plus an optional re-XADD, so a rejected message reappears at the tail of the stream with no priority and the same RedeliveryStamp logic. Ordering guarantees you relied on with AMQP won't hold for retries.
Keep the transport serializer stable across deploys. Messages sitting in the stream were encoded by the old code. Changing the transport serializer, or bumping a message class's property names, strands in-flight messages as undecodable. Drain the stream before breaking changes, or version your message classes.
Blocking reads and container shutdown. Long block timeouts mean slow restarts and slow deploys. Keep block timeouts in the low seconds and let the worker loop poll; combine with --time-limit so orchestration restarts are clean.
Redis Cluster pins the whole stream to one shard. Consumer groups are per-key, so all traffic for one stream hits one node. You scale horizontally by adding streams, not by sharding one.
Setup permissions. XGROUP CREATE fails if the group already exists, so messenger:setup-transports should be treated as idempotent-but-noisy. Managed Redis offerings sometimes restrict XGROUP/XINFO — confirm your provider allows them, or provisioning will fail at deploy time.
Upgrade hygiene and maturity. 1.2.2 is a conservative patch — Symfony 7 constraint support only, no behavioural changes — so upgrading from 1.2.1 is low risk; run composer why-not handcraftedinthealps/redis-transport-bundle 1.2.2 if the solver refuses, since the usual culprit is a transitive Symfony 5.4-only dependency. The package is still small, single-maintainer territory: pin the version, read the release diff before upgrading (these are short and readable), and keep a fallback — Symfony's own Redis transport covers the same streams primitive if you ever need to swap the DSN back.
Extension points. The transport factory and connection service are the places to decorate — custom options handling, a Predis-vs-phpredis switch, or a metrics decorator around get()/ack() for lag reporting. Wire it with a compiler pass overriding the factory service id rather than forking the bundle.
How can I help you explore Laravel packages today?