Message Queues in Production: Patterns for Reliable Pipelines
When a CI job fails mid-way, do you retry manually?
When a webhook from a third-party service arrives while your API is down, is the event lost?
If either answer is yes, you need message queuing — not as a developer pattern, but as an operational reliability mechanism.
The Core Pattern: Producer / Consumer
%%{ init: { 'look': 'handDrawn' } }%%
graph LR
A[Producer<br/>API / Webhook / CI Job] -->|Publish message| B[Message Queue<br/>RabbitMQ / SQS / Kafka]
B -->|Consume| C[Worker 1]
B -->|Consume| D[Worker 2]
B -->|Consume| E[Worker N]
C -->|Acknowledge| B
D -->|Acknowledge| B
E -->|Acknowledge| B
The queue decouples the producer (which just needs to know “my event was received”) from the consumer (which processes at its own pace, with retries).
Why This Matters in DevOps
Consider a GitOps deployment pipeline:
- A developer pushes to
main - GitHub Actions publishes a
deployevent to the queue - ArgoCD (the consumer) picks it up and syncs the cluster
If ArgoCD is temporarily unavailable, the event sits in the queue. It doesn’t get lost. When ArgoCD recovers, it processes the backlog. No manual intervention.
Queue Semantics: At-Least-Once vs Exactly-Once
%%{ init: { 'look': 'handDrawn' } }%%
graph TD
A[Message published] --> B{Queue type}
B -->|At-least-once<br/>RabbitMQ, SQS| C[Deliver to worker]
C --> D[Process]
D --> E{Ack?}
E -->|Yes| F[Remove from queue]
E -->|No / Timeout| C
B -->|Exactly-once<br/>Kafka| G[Write to log]
G --> H[Deliver once<br/>deduplicated]
At-least-once (RabbitMQ, SQS): The message is delivered at least once. Your worker must be idempotent — processing the same message twice produces the same result. Design for this.
Exactly-once (Kafka): More complex guarantees, higher latency. Use when duplicate processing has real consequences (e.g., billing events).
Queue Services and When to Use Them
| Service | Model | Best For |
|---|---|---|
| RabbitMQ | Brokered, rich routing | Complex routing rules, multi-protocol |
| Redis Pub/Sub | Fire-and-forget | Real-time fan-out, not durable |
| Apache Kafka | Append-only log | High-throughput event streaming |
| AWS SQS | Managed, fully hosted | AWS ecosystems, zero ops |
| Google Cloud Pub/Sub | Managed, global | GCP ecosystems, multi-region |
Production Checklist
%%{ init: { 'look': 'handDrawn' } }%%
graph TD
A[Queue deployed] --> B[Dead letter queue configured?]
B -->|No| C[Messages lost on consumer failure]
B -->|Yes| D[Retry policy set?]
D -->|No| E[Infinite retry loops]
D -->|Yes| F[Monitoring: queue depth alert?]
F -->|No| G[Silent failures]
F -->|Yes| H[Alert on depth > threshold]
Before going to production:
- Dead letter queue (DLQ): Messages that fail after N retries go here, not into the void
- Retry backoff: Exponential backoff prevents thundering herd when a service recovers
- Queue depth monitoring: Alert when the queue grows faster than consumers can drain it
- Consumer concurrency: Can multiple workers process in parallel? (Most queues support this)
The Operational Mindset Shift
Queues aren’t about making your code asynchronous. They’re about making your system resilient to failure. When your API goes down, queued events wait. When your CI runner restarts, the job sits in the queue. When a consumer crashes mid-processing, the message is redelivered.
That’s the DevOps perspective on queuing: not “how do I make my app faster” but “how do I make my system survive reality.”