rsqueue/rsqueue-bundle
RSQueue Bundle adds a Redis-backed queue to PHP applications: push jobs, run workers, schedule delayed tasks, and monitor throughput with simple configuration. It scales from single-process scripts to distributed worker pools without extra infrastructure.
RSQueue is a thin Symfony2-era wrapper around Redis lists that gives you a producer/consumer queue with a worker command. Treat it as plumbing, not a framework: the whole surface area is "push a job", "run a worker", "handle a job".
Minimal setup:
composer require rsqueue/rsqueue-bundle
app/AppKernel.php:
new RSQueue\Bundle\RSQueueBundle(),
DependencyInjection/Configuration.php in the repo; it is typically rs_queue: with host, port, database and a default queue/tube name. Point it at the same Redis you already use for cache/sessions or a dedicated DB index.app/console list rsqueue # confirm the exact command name in your version
app/console rsqueue:worker:run
Look first at: the bundle's Command/ directory (worker loop, retry/status commands), the Configuration.php (every knob you have), and any Resources/config/services.xml (which services you can override or decorate).
Producer from a controller or service. The producer is a service you inject; push a job with a small payload:
$this->get('rs_queue.producer')->push('mail', [
'userId' => $user->getId(),
'template' => 'welcome',
]);
Prefer injecting the producer service into your own service rather than pulling from the container, so the same code works from controllers, console commands, and fixtures.
Payload discipline: pass IDs, not entities. Serialize scalars/arrays only. Passing a Doctrine entity means the worker — a separate PHP process with a separate EntityManager — will either fail to unserialize or operate on a stale, detached object. Fetch fresh state inside the handler by ID.
Handlers as small, single-purpose classes. Keep one handler per job type, handle(array $payload), no dependencies on the HTTP request. Wire them either via the bundle's handler registration (if your version supports a handler map) or a simple switch in a single consumer class while you're proving the idea out. The switch version is ugly but has zero configuration surface and is a fine first iteration.
Multiple queues for priority. Use separate queue/tube names for critical, default, and bulk, and run separate worker processes against each. This is the cheapest way to keep a 10-minute report export from starving password-reset emails. It also gives you independent scaling and independent monitoring per priority.
Schedule with delay rather than cron. For "send this reminder in 24h", the bundle's delayed-push (if present in your version — check the producer API) or a score-based Redis sorted set is usually better than a cron job that scans the database. If the bundle lacks it, keep cron for coarse scheduling and the queue for execution.
Supervise workers properly. Never run the worker from an interactive shell in production. Use systemd or Supervisor with autorestart=true and enough processes to keep up with the fast queue; the worker is a long-running process, so also set a max-jobs or max-runtime and let the supervisor recycle it (memory leaks are the default state of long-running PHP).
Bound the request path. Push and return. Don't ->get() on a result, don't wait for completion, don't let a Redis hiccup turn a 200 into a 500 — wrap the push in a try/catch and log, or fall back to doing the work inline if the queue is unavailable and the operation is cheap.
Monitoring you can build in an afternoon. Queue depth is LLEN <queue> in Redis; the worker's throughput is pushes-minus-pops over time. Expose both to your monitoring and alert on depth growing monotonically — that's the signal that workers are dead or too slow, and it's far more useful than any dashboard the bundle ships.
This package is effectively unmaintained. Last release 2018, ~11 stars, zero dependents. It targets Symfony 2 (AppKernel, get('service') container access, old command signatures) and will not install cleanly on Symfony 4+ without forking and rewriting the service definitions. Before adopting: check the constraint on symfony/symfony in composer.json, confirm it installs in your app, and read the open issues. If you're on a modern Symfony, Symfony Messenger with the Redis transport is the maintained equivalent and you should use that instead. Consider this bundle only if you're already pinned to a legacy Symfony2 codebase.
Redis is not a durable queue. A Redis restart, an eviction under maxmemory, or a failover can lose jobs silently. Enable AOF (appendfsync everysec at minimum), keep queue data on a dedicated Redis instance or DB index not subject to cache eviction, and make every handler idempotent — at-least-once delivery plus retries means duplicates happen. Guard with a "job already processed" key or a unique constraint.
Failure handling is likely your job. Check your version for retry/dead-letter support. If it's absent, wrap the handler body in try/catch, log with the payload, and push to a failed queue yourself rather than letting the exception kill the worker loop. A worker that dies on one bad job is a silent outage.
Unserialize is an attack surface. If payloads ever become PHP serialize()d objects, a compromised Redis is remote code execution via object injection. Keep payloads JSON/arrays, and if you must accept objects, only from trusted internal producers.
Blocking pops need care with shutdown. The worker typically blocks on BRPOP/BLPOP. That's efficient, but it means SIGTERM may not be honored until the blocking call returns — use a finite timeout on the pop (e.g. one second) so the loop checks a stop flag between iterations, and give Supervisor a generous stopwaitsecs.
Config quirks to verify, not assume: the config root key, whether database is an index or a name, whether the queue name is global or per-push, and whether the connection is lazily created. A wrong database index is the classic "jobs go in, nothing comes out" bug — confirm with redis-cli -n <db> LLEN <queue>.
Debugging loop:
redis-cli -n <db> MONITOR # watch pushes/pops live
redis-cli -n <db> LLEN <queue> # depth
redis-cli -n <db> LRANGE <queue> 0 5
If depth rises but the worker is idle, your worker is pointed at a different database or queue name, not broken.
Worker memory and Doctrine. Clear the EntityManager inside the job loop ($em->clear()), or you accumulate managed entities until OOM. Same for any service that caches per-request state.
Extension points worth knowing: you can decorate/replace the producer service to add logging, metrics, or a payload envelope (job id, attempt count, enqueued-at) without touching call sites. Subclassing or decorating the worker command is the cleanest way to add per-job timing and structured error output. Do that before you fork the bundle for anything non-trivial — it keeps the upgrade path (to Messenger) open.
How can I help you explore Laravel packages today?