Message queues
Backend Fundamentals · Messaging & Event-Driven Architecture · 9 min read
1. What problem does this solve?
| Problem | Why it matters | Consequence if ignored | Real‑world example |
|---|---|---|---|
| Decoupling | Producers and consumers can evolve independently. | Tight coupling forces synchronous calls, leading to cascading failures. | A user sign‑up service that triggers email verification via a separate mailer service. |
| Load leveling | Handles traffic bursts by buffering work. | Immediate spikes can overwhelm downstream services, causing timeouts or crashes. | A flash sale that pushes millions of order events into a payment service. |
| Reliability | Guarantees message delivery even if consumers fail. | Lost messages mean lost data, inconsistent state, or missed business events. | A financial transaction that must be persisted even if the processing node dies. |
| Scalability | Enables horizontal scaling of consumers without changing producers. | All services must scale together, increasing cost and complexity. | A recommendation engine that can spin up more workers during peak hours. |
| Asynchronous processing | Offloads long‑running work from request/response cycles. | Users experience slow responses or timeouts. | Image resizing after upload. |
Message queues are the backbone of many modern backend systems: order processing pipelines, event sourcing, log aggregation, background job queues, and microservice communication.
2. Mental model
Analogy: The Postal Service
- Producers are people who write letters (messages).
- Queues are post offices that hold letters until a courier (consumer) picks them up.
- Consumers are couriers that deliver letters to recipients.
- Broker is the national postal network that routes letters between post offices.
Just as the postal system decouples the sender from the receiver, a message queue decouples the producer from the consumer. The sender can finish its work immediately after dropping the letter, while the receiver processes it at its own pace.
Engineering model
Producer → Broker (queue) → Consumer
- Producer publishes a message to a topic or queue.
- Broker stores the message durably and makes it available to consumers.
- Consumer pulls (or is pushed) the message, processes it, and acknowledges receipt.
3. Deep explanation
Internals
| Component | Responsibility |
|---|---|
| Broker | Persists messages, manages queues/topics, handles replication and failover. |
| Producer | Serializes data, sends to broker, optionally sets message attributes (TTL, priority). |
| Consumer | Pulls messages, processes, acknowledges or rejects. |
| Queue/Topic | Logical buffer; may be a simple FIFO queue or a partitioned log. |
| Offset / Sequence | Tracks consumption progress; crucial for at‑least‑once semantics. |
| Dead‑Letter Queue (DLQ) | Stores messages that failed repeatedly. |
Important terminology
- Producer – entity that sends messages.
- Consumer – entity that receives messages.
- Broker – the message‑queue server (e.g., RabbitMQ, Kafka, Redis Streams).
- Queue – a FIFO buffer (RabbitMQ, SQS).
- Topic – a publish/subscribe log (Kafka, Redis Streams).
- Partition – a slice of a topic that allows parallel consumption.
- Consumer Group – a set of consumers that share a topic’s partitions.
- Acknowledgement (Ack) – consumer confirms successful processing.
- At‑least‑once – message may be delivered multiple times but never lost.
- At‑most‑once – message delivered once or not at all (risk of loss).
- Exactly‑once – hard to guarantee; often achieved via idempotent consumers.
Trade‑offs
| Feature | Pros | Cons |
|---|---|---|
| Durability | Messages survive broker restarts. | Disk I/O overhead, slower throughput. |
| Ordering | Guarantees order within a partition. | Requires single consumer per partition; limits parallelism. |
| Throughput | High with batching and prefetch. | Requires careful tuning; risk of over‑loading consumers. |
| Latency | Low when broker is local. | Network hops add latency; persistence adds delay. |
| Exactly‑once | No duplicates. | Complex; often requires idempotent consumers or transactional support. |
Failure modes
- Broker crash – messages may be lost if not persisted or replicated.
- Consumer crash – unacked messages re‑queued; can lead to duplicates.
- Network partition – split‑brain; some consumers may see stale data.
- Message corruption – malformed payloads can crash consumers.
Limitations
- Eventual consistency – consumers may lag behind producers.
- No built‑in transaction across services – requires application‑level choreography.
- Limited queryability – cannot easily search past messages without external indexing.
Where it fits
- Microservice communication – async request/response or event bus.
- Background jobs – offload CPU‑heavy work.
- Event sourcing – store domain events in a log.
- Data pipelines – stream data between ingestion, processing, and storage layers.
Interaction with related concepts
| Concept | Interaction |
|---|---|
| Database | Use DB writes as part of message payload or as a source of truth. |
| Cache | Cache results of expensive computations triggered by messages. |
| HTTP | Producers may expose REST endpoints that enqueue work. |
| AI backends | Queue inference requests to scale GPU workers. |
| Redis | Redis Streams can act as lightweight queues. |
| Kafka | Provides high‑throughput, partitioned logs for analytics. |
4. How it works
- Producer serializes a payload and sends it to the broker’s queue/topic.
- Broker writes the message to disk (or memory) and assigns a unique ID/offset.
- Consumer pulls the message (pull) or receives it via push.
- Consumer processes the payload.
- Consumer sends an ack back to the broker.
- Broker removes the message from the queue (or marks it as consumed).
- If ack is not received within a timeout, the broker re‑queues the message.
sequenceDiagram
participant P as Producer
participant B as Broker
participant C as Consumer
P->>B: Publish(message)
B->>B: Persist(message)
B->>C: Deliver(message)
C->>C: Process(message)
C->>B: Ack(message)
B->>B: Remove(message)
5. Practical implementation
5.1 RabbitMQ with amqplib (Node.js)
// rabbitmq.ts
import amqp from 'amqplib';
const RABBIT_URL = 'amqp://localhost';
const QUEUE = 'orders';
export async function publishOrder(order: any) {
const conn = await amqp.connect(RABBIT_URL);
const ch = await conn.createChannel();
// Ensure the queue exists
await ch.assertQueue(QUEUE, { durable: true });
// Serialize and send
const msg = Buffer.from(JSON.stringify(order));
ch.sendToQueue(QUEUE, msg, { persistent: true });
console.log('Order queued:', order.id);
await ch.close();
await conn.close();
}
// consumer.ts
import amqp from 'amqplib';
const RABBIT_URL = 'amqp://localhost';
const QUEUE = 'orders';
async function consume() {
const conn = await amqp.connect(RABBIT_URL);
const ch = await conn.createChannel();
await ch.assertQueue(QUEUE, { durable: true });
// Prefetch 1 to avoid overloading the consumer
ch.prefetch(1);
console.log('Waiting for orders...');
ch.consume(
QUEUE,
async (msg) => {
if (!msg) return;
const order = JSON.parse(msg.content.toString());
try {
// Process order (e.g., charge payment, update inventory)
await processOrder(order);
ch.ack(msg); // Success
} catch (err) {
console.error('Processing failed', err);
ch.nack(msg, false, false); // Drop or requeue based on policy
}
},
{ noAck: false }
);
}
async function processOrder(order: any) {
// Simulate async work
await new Promise((r) => setTimeout(r, 200));
console.log('Processed order', order.id);
}
consume().catch(console.error);
Key points
persistent: trueensures the message survives broker restarts.prefetch(1)limits unacked messages to one, preventing a slow consumer from blocking others.ack/nackcontrol message removal or requeue.
5.2 Redis Streams (ioredis)
// redis-stream.ts
import Redis from 'ioredis';
const redis = new Redis(); // defaults to localhost:6379
const STREAM = 'orders';
const GROUP = 'order-processor';
const CONSUMER = 'worker-1';
async function produce(order: any) {
await redis.xadd(
STREAM,
'*',
'id',
order.id,
'amount',
order.amount.toString()
);
console.log('Order added to stream', order.id);
}
async function consume() {
// Create consumer group if not exists
try {
await redis.xgroup('CREATE', STREAM, GROUP, '