The Queue Is Not a Buffer
A B2B integration API hit 2M events per day. Endpoints timed out during partner spikes. Someone added RabbitMQ.
Latency dropped roughly 60%. Then messages piled up nobody watched. Retries doubled charges. On-call learned a new 3am page.
A queue is not a performance trick. It is a contract about failure, order, and time.
Wrong Reasons to Queue
“Make the endpoint faster.”
Sometimes true. Often you moved latency to a worker and lost visibility.
“We might spike later.”
Spikes need backpressure and consumer capacity — not a deeper queue.
Queue when you need to:
- decouple producers from slow consumers
- absorb bursts without dropping requests
- retry work safely when failures are transient
If none of those apply, fix the hot path first.
What Breaks in Production
Poison messages — one bad payload retries forever.
Duplicate processing — at-least-once delivery without idempotency.
Prefetch too high — one slow job blocks prefetched work behind it.
No dead-letter path — failures clog the main queue or vanish.
The broker did not fail. The design around it did.
Technology Stack
| Layer | Choice | Why |
|---|---|---|
| Broker | RabbitMQ | Durable queues, DLX, at-least-once, proven at high volume |
| App | Laravel Queue | ShouldQueue jobs, retries, failed-job table |
| Driver | rabbitmq queue connection |
Native AMQP, not Redis pretending to be a queue |
| Idempotency | Redis or DB processed_events |
Dedupe by event_id before side effects |
| Ops | RabbitMQ Management + alerts | Queue depth, consumer util, DLQ rate |
| Deploy | Horizon (if Redis aux queues) or supervisor | Process workers; RabbitMQ workers via queue:work rabbitmq |
Do not use the queue as your database. Messages are for delivery, not source of truth.
A Job That Survives
Dispatch from the HTTP boundary:
// app/Jobs/ProcessPartnerEvent.php
final class ProcessPartnerEvent implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 5;
public array $backoff = [10, 30, 60, 120, 300];
public function __construct(public string $eventId, public array $payload) {}
public function handle(PartnerEventProcessor $processor): void
{
if (ProcessedEvent::where('event_id', $this->eventId)->exists()) {
return; // idempotent — safe on redelivery
}
$processor->run($this->payload);
ProcessedEvent::create(['event_id' => $this->eventId]);
}
public function failed(Throwable $e): void
{
// lands in failed_jobs — route to DLQ review workflow
report($e);
}
}
RabbitMQ dead-letter exchange (concept):
main queue: events
→ on reject / TTL exceeded → DLX → events.dlq
Configure x-dead-letter-exchange on the main queue. Inspect DLQ daily. Poison messages belong there, not in infinite retry.
Three rules:
- Idempotent by
event_id - Bounded retries with backoff
- Failed jobs visible and reviewable
Pair with Idempotency-Key at the HTTP boundary. You need both.
What to Watch
Worker CPU is the wrong metric.
| Metric | Source | Alert when |
|---|---|---|
| Queue depth | RabbitMQ Management | Growing for >15 min |
| Consumer lag | Oldest ready message age | > SLA threshold |
| DLQ rate | events.dlq publish rate |
Any sustained spike |
| Job fail rate | Laravel failed_jobs |
Above baseline |
| Process p95 | App logs per job class | Jumps after deploy |
rabbitmq_queue_messages_ready{queue="events"} > 10000
rabbitmq_queue_consumer_utilisation < 0.5 # consumers idle while depth grows — bug
Alert on lag and DLQ. Not on “workers look busy.”
When It Paid Off
Moving synchronous partner calls to RabbitMQ cut end-to-end latency roughly 60% — because:
- HTTP threads stopped waiting on slow partners
- Workers scaled independently on AWS
- Retries no longer blocked ingest
- Spikes flattened instead of timing out
The win was architecture. RabbitMQ was the tool.
Key Takeaways
Queues decouple and retry. They do not fix slow code by default.
Use RabbitMQ + Laravel jobs with bounded retries and idempotent handlers.
Configure dead-letter routing. Inspect failed jobs.
Watch queue depth and message age — not CPU.
Queue for decouple, burst, and safe retry — not because the diagram looks modern.