For a long time, “the queue” meant Laravel’s queue system. Redis, database, maybe SQS. A job class, a ShouldQueue, a worker process, retry_after, and the occasional failed_jobs row I would look at when something felt off.
RabbitMQ is not a driver you forget about. It is a broker with its own vocabulary, and that vocabulary exists because the problems are real.
This is the note I needed when I stopped treating messaging as a Laravel feature and started treating it as infrastructure.
Laravel queues hide a lot, on purpose
That is not an insult. A good framework should hide the boring parts.
In Laravel you usually get:
- a job serialized onto a backend
- a worker that pops it
- retries with backoff
- a failed job table
You can ship a lot of production software without knowing whether you are doing competing consumers, pub/sub, or a work queue. Redis lists will carry you surprisingly far.
RabbitMQ makes you name the parts:
- producer
- exchange
- binding
- queue
- consumer
- ack / nack
Once you see those, Laravel’s queue starts looking like one opinionated topology: a direct-ish work queue with retries bolted on in the application.
The mental model that finally stuck
A message does not “go to a queue” in the way a Laravel job goes to redis. A message goes to an exchange. The exchange, using bindings and routing keys, decides which queues receive a copy.
That one sentence explains a surprising amount of confusion:
- Why can two consumers see the same logical event? Because two queues were bound.
- Why did nothing consume my message? Because the routing key did not match a binding.
- Why is a fanout exchange “broadcast”? Because that is its whole job.
Acknowledgements are not retries
This bit cost me time.
In Laravel, if a job throws, the worker typically releases it or marks it failed according to the job’s tries. You think in application exceptions.
In RabbitMQ, if a consumer dies without acking, the broker can redeliver. That is not the same as tries = 3. It is a statement about whether the broker still considers the message in-flight.
Nack with requeue, nack without requeue, dead-letter exchanges - these are policy. If you do not choose a policy, you will get a default you did not mean.
A practical rule I use now:
- Ack only after the side effect you care about has happened, or has been recorded as happened.
- If the side effect is not idempotent, you do not have a retry strategy. You have a duplicate-risk strategy.
Laravel jobs have the same problem. RabbitMQ just refuses to let you ignore it.
What I still use Laravel queues for
Not everything should be a broker topology.
If the work is “send this email after the request”, Laravel on Redis is simple and honest. If the work is “several independently deployed services need to react to an order without sharing a database”, a broker starts to earn its keep.
The mistake is fashion. I have seen RabbitMQ introduced because it sounded distributed. I have also seen Redis queues asked to be an integration bus. Both are ways of using the wrong sentence for the problem.
A small map
| Need | I would start with |
|---|---|
| App-internal background work | Laravel queue (Redis) |
| Work that must survive worker death with explicit routing | RabbitMQ work queue |
| Multiple independent consumers of the same event | RabbitMQ with a fanout or topic exchange |
| Cloud-native, less operational broker | SQS, with eyes open about ordering |
I am still learning the sharp edges: prefetch, poison messages, and the difference between “the queue is deep” and “the consumer is slow”. Related: RabbitMQ redelivery.