Message Queue Types: Point-to-Point vs Publish-Subscribe
Understand the two fundamental messaging patterns - point-to-point and publish-subscribe - and when to use each, including JMS, AMQP, and MQTT protocols.
Message queues enable asynchronous communication between services through two patterns: point-to-point (each message goes to exactly one consumer) and publish-subscribe (each message goes to all subscribers). The pattern you pick shapes failure handling, ordering guarantees, and scalability characteristics. At-least-once delivery with idempotent consumers hits the practical sweet spot — more reliable than fire-and-forget, simpler than exactly-once. After reading, you'll understand delivery semantics (QoS 0/1/2), how to handle poison messages with dead letter queues, and when to reach for RabbitMQ, Kafka, or a cloud-managed solution.
Message queues are the backbone of asynchronous communication in distributed systems. They let services produce and consume messages without tight coupling or direct dependencies. Choosing the right queue type — point-to-point or pub/sub — shapes how your system handles failures, ordering, and scalability. The Publish/Subscribe Patterns post covers the pub/sub model in depth, while this guide focuses on the fundamentals both patterns share.
Introduction
Message queues are the backbone of asynchronous communication in distributed systems. They enable services to communicate without waiting for immediate responses, providing decoupling, reliability, and scalability.
This post covers the two fundamental messaging patterns — point-to-point and publish-subscribe — their delivery guarantees, operational considerations, and how to choose the right approach for your use case.
For implementations, see our posts on RabbitMQ, Apache Kafka, and AWS SQS/SNS.
Core Concepts
In point-to-point (P2P) messaging, each message goes to exactly one consumer. The queue holds messages until a consumer picks them up, then removes the message. If no consumer is available, the message waits.
graph LR
Producer[Producer] -->|message| Q[Queue]
Q -->|message 1| Consumer1[Consumer 1]
Q -->|message 2| Consumer2[Consumer 2]
Q -->|message 3| Consumer3[Consumer 3]
This pattern is useful for task distribution. Think of a queue of print jobs: each job goes to one printer, not all printers. The sender does not care which consumer handles it, only that someone does.
Point-to-Point
In the point-to-point model, each message has exactly one destination. The producer sends to a named queue, and one consumer from a pool picks it up. Once consumed, the message is removed - no other consumer can retrieve it. This is the classic work queue pattern that most people think of when they hear “message queue.”
Point-to-Point Key Characteristics
- Each message goes to exactly one consumer
- Messages wait in the queue until a consumer picks them up
- Producers can outpace consumers; the queue absorbs the difference
- No fan-out—messages cannot automatically go to multiple consumers
Point-to-Point Common Use Cases
- Task processing like image resizing or video transcoding
- Background job queues
- Decoupling requesters from responders
- Load balancing across workers
Publish-Subscribe Messaging
Publish-subscribe (pub/sub) is a different model. Messages are published to a topic, and all subscribers to that topic receive a copy.
graph LR
Producer[Publisher] -->|message| Topic[Topic]
Topic -->|message| Consumer1[Subscriber 1]
Topic -->|message| Consumer2[Subscriber 2]
Topic -->|message| Consumer3[Subscriber 3]
The publisher has no idea who is listening. Subscribers opt into topics, and every matching message goes to all of them.
Topic Hierarchies
Many pub/sub systems support hierarchical topic structures. The idea is simple: subscribers pick the level in the tree that matches what they want.
orders/
orders/created
orders/updated
orders/cancelled
orders/fulfilled
Subscribe to orders and you get everything under it. Subscribe to orders/created and you get only creation events. New event types under orders/ automatically reach the broader subscriber without anyone changing their subscription.
Naming discipline is the real cost here. Without a shared convention, orders.created and order.created become two different topics that cannot be merged. Pick a convention early and enforce it through code review or tooling.
Wildcard support varies by broker. Some let you subscribe to orders.* for all order events, or orders.# for nested subcategories. Others require explicit enumeration. If you assume wildcards and later switch brokers, every subscriber breaks.
Pub/Sub
In the pub/sub model, messages go to a topic instead of a queue. Every subscriber to that topic gets a copy. The publisher does not know who the subscribers are or what they do with the message. Subscribers opt in independently, and new subscribers can join without the publisher changing anything.
Pub/Sub Key Characteristics
- One-to-many delivery: each message goes to all subscribers
- Topic-based filtering: subscribers choose what to receive
- No built-in message persistence: most systems don’t store messages for offline subscribers
- Fan-out: the same message reaches multiple consumers
Pub/Sub Common Use Cases
- Broadcasting events like user signups or order placements
- System-wide notifications
- Replicating data across services
- Pushing real-time updates to multiple clients
Comparing the Patterns
| Aspect | Point-to-Point | Publish-Subscribe |
|---|---|---|
| Delivery | One consumer per message | All subscribers per message |
| Coupling | Producer to queue to consumer | Publisher to topic to subscribers |
| Data flow | Single consumer | Fan-out to all subscribers |
| State | Queue holds messages | Topics typically transient |
| Use case | Task distribution | Event broadcasting |
Delivery Guarantees
Message queues offer different delivery guarantees. The semantics you pick directly affect how your consumers handle failures and duplicates.
Delivery Guarantees Overview
Message brokers offer three levels of delivery guarantee. Each one balances reliability against throughput. The right choice depends on how much data loss your application can tolerate and how expensive duplicates are to handle.
At-Most-Once Delivery (QoS 0)
The broker fires the message at the consumer and does not wait for acknowledgment. If the consumer crashes before processing, the message is gone. This is the “fire and forget” model.
Use when: You can afford to lose messages occasionally. Sensor data where freshness matters more than completeness fits here.
At-Least-Once Delivery (QoS 1)
The broker holds the message until the consumer acknowledges it. If the consumer times out or crashes, the message gets redelivered. You may see duplicates.
Use when: Missing messages is worse than processing duplicates. Billing reconciliation, order processing—anywhere you need confirmation that the message was handled.
Exactly-Once Delivery (QoS 2)
A two-phase protocol prevents both loss and duplicates. The broker and consumer negotiate delivery in two hops: first a prepare, then a commit. This costs throughput but eliminates duplicate processing.
Use when: Financial transactions where duplicates have real consequences. The performance hit is substantial, so most systems settle for at-least-once with idempotent consumers.
graph LR
subgraph "At-Most-Once"
A1[Broker sends] --> A2[Consumer receives]
A2 -.-> A3[Crash = message lost]
end
subgraph "At-Least-Once"
B1[Broker sends] --> B2[Consumer ACKs]
B2 -.-> B3[Timeout = redeliver]
end
subgraph "Exactly-Once"
C1[Prepare] --> C2[Commit]
C2 --> C3[No loss, no duplicate]
end
Idempotent Consumers
To get exactly-once behavior without QoS 2 overhead, make your message processing idempotent. Use a unique message ID as a deduplication key. Store processed IDs in Redis or a database with a short TTL. If you see the same ID twice, skip processing.
// Idempotent message processor
public void processMessage(Message msg) {
String msgId = msg.getMessageId();
if (processedIds.contains(msgId)) {
return; // Already handled, skip
}
// ... do the actual work ...
processedIds.add(msgId);
}
Fault Tolerance
Messages that fail repeatedly need somewhere to go. Dead letter queues (DLQs) catch them so they do not block the main queue.
Fault Tolerance Overview
Message queue systems face failures at multiple levels. A broker can crash and lose in-flight state. A consumer can go down mid-processing, leaving messages half-consumed. The network between them can stall or partition. Each failure mode needs a different strategy.
Retries are the first line of defense. When a consumer fails to acknowledge a message, the broker redelivers it after a configurable timeout. You can set the retry count and backoff schedule. A typical pattern starts with short delays and backs off to longer intervals. After the maximum delivery attempts are exhausted, the message moves to a dead letter queue.
Dead letter queues catch what retries cannot handle. The DLQ stores the original message payload plus failure metadata: retry count, exception type, timestamps. Operators watch the DLQ to find and fix recurring issues.
Brokers also provide broader fault tolerance mechanisms:
- Durable queues write messages to disk so they survive broker restarts
- Clustering provides failover when a node dies
- Replication copies data across nodes so the system can tolerate losing a machine
Each mechanism trades reliability against throughput. Choose the level that matches your recovery requirements.
How DLQs Work
When a message exceeds your retry limit or fails with a permanent exception, the broker moves it to a DLQ instead of discarding it. The DLQ holds the original message plus metadata about the failure: exception type, error message, retry count, timestamps.
Configuring Dead Letter Queues
Most brokers let you configure DLQ behavior per queue:
// ActiveMQ Artemis DLQ configuration
QueueConfiguration config = new QueueConfiguration()
.setName("orders.queue")
.setDeadLetterAddress("orders.dlq")
.setMaxDeliveryAttempts(5)
.setDeadLetterQueueDelay(60000); // Wait 1 min before moving to DLQ
artemis.createQueue(config);
# RabbitMQ dead letter exchange setup
channel.exchange_declare(exchange='orders.dlx', exchange_type='direct')
channel.queue_declare(queue='orders.dlq')
channel.queue_bind(queue='orders.dlq', exchange='orders.dlx', routing_key='dead')
# Main queue with DLX
channel.queue_declare(
queue='orders',
arguments={
'x-dead-letter-exchange': 'orders.dlx',
'x-dead-letter-routing-key': 'dead',
'x-message-ttl': 86400000 # 24 hour TTL
}
)
DLQ Monitoring and Processing
DLQs need active monitoring. Set up alerts:
- Alert when DLQ depth exceeds zero
- Log DLQ arrivals with failure context
- Have a process to investigate and retry or discard DLQ messages
- Consider a separate DLQ consumer that can route messages back to the main queue after fixes
Poison Message Handling
Some messages keep failing no matter what. These “poison messages” can lock up a queue if they always get redelivered. Set maxDeliveryAttempts to put a hard limit on retries. After that, the DLQ takes over.
Flow Control
When producers outpace consumers, you need strategies to handle overflow.
Flow Control Overview
When producers push messages faster than consumers can keep up, the system needs backpressure. Without it, consumers run out of memory or crash. Message brokers offer several backpressure strategies, from prefetch limits to circuit breakers.
Prefetch Limits
Brokers like RabbitMQ let you limit how many messages a consumer has “in flight” at once. Set prefetch=10 and the broker stops sending after 10 unacknowledged messages. The consumer catches up before getting more. This prevents memory exhaustion on slow consumers.
// RabbitMQ prefetch configuration
channel.basicQos(10); // Max 10 unacked messages
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) {
// Process message
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
Flow Control in Kafka
Kafka consumers control their own read pace by committing offsets. If a consumer falls behind, it just means lag—more data sitting on disk waiting to be read. Monitor consumer lag as a key metric. If lag grows faster than you can catch up, add consumer instances to the group.
# Check consumer lag
kafka-consumer-groups.sh --bootstrap-server localhost:9092 \
--group my-consumer-group --describe
# Output shows current lag per partition
# TOPIC PARTITION CURRENT-OFFSET LOG-END-OFFSET LAG
# orders 0 5000 5500 500
Message Throttling
Some systems let you slow down producers when the queue fills. RabbitMQ has per-connection credit flow—producers pause when the broker runs low on resources. This is gentler than hard rejections when the queue is full.
Consumer Scaling
You can add more consumers to catch up. For Kafka, add more partitions and spread the load. For queue-based systems, add more worker instances. Just make sure your consumers are stateless so they can run in parallel.
Circuit Breakers
When consumers start failing (database down, API timeout), do not keep hammering them with messages. Implement circuit breakers that pause consumption when error rates spike. The queue holds messages while you recover.
Ordering Guarantees
Some workloads need messages processed in order. Here is what different brokers actually guarantee.
Ordering Guarantees Overview
Message ordering often trades off against parallelism. Different brokers and patterns offer different guarantees. The right choice depends on whether your consumers can handle out-of-order messages and how much throughput you need.
FIFO Queues
True FIFO requires a single consumer per queue. With one consumer, messages leave in the same order they arrived. This is the simplest case, but gives up parallelism.
Partitioned Topics
Kafka provides ordering within a partition. If you partition by orderId, all events for the same order go to the same partition, and that partition is processed by exactly one consumer. You get ordering plus parallelism.
Topic: orders
Partitions: 3 (partitioned by orderId % 3)
order-101 → partition 1
order-102 → partition 2
order-103 → partition 3
order-104 → partition 1 (same partition as 101, processed in order)
Sequence Numbers
For systems that need global ordering across multiple consumers, embed a sequence number in each message. The consumer tracks the highest sequence number it has seen and rejects any message with a lower number.
public void processMessage(Message msg) {
long seq = msg.getSequenceNumber();
if (seq <= lastProcessedSeq) {
return; // Out of order, skip
}
lastProcessedSeq = seq;
// Process the message
}
Pattern Comparison
| Pattern | Ordering | Parallelism | Complexity |
|---|---|---|---|
| Single consumer queue | Full FIFO | None | Low |
| Partitioned topic (1 partition) | FIFO within partition | Low | Medium |
| Partitioned topic (N partitions) | FIFO per partition | N consumers | Medium |
| Sequence numbers | Global ordering | Any | High |
Trade-off Analysis
| Dimension | Point-to-Point | Publish-Subscribe |
|---|---|---|
| Delivery | Exactly one consumer | All subscribers receive copy |
| Scalability | Load leveling across workers | Fan-out to many subscribers |
| Fault Tolerance | Queue absorbs producer spikes | Subscribers must stay online |
| Ordering | FIFO with single consumer | No ordering guarantee |
| Complexity | Simple queue setup | Topic subscription management |
| Use Case | Task distribution, work queues | Event broadcasting, notifications |
| Backpressure | Prefetch limits + DLQs | Slow subscribers lose messages |
| Monitoring | Queue depth + consumer lag | Subscriber count + topic metrics |
Protocol Comparison
Here is how the major messaging protocols stack up:
| Feature | AMQP 1.0 | AMQP 0-9-1 | MQTT | CoAP |
|---|---|---|---|---|
| Model | Point-to-point + pub/sub | Point-to-point + pub/sub | Primarily pub/sub | Request/response |
| Wire protocol | Binary | Binary | Binary | Binary |
| Header overhead | ~40 bytes | ~40 bytes | ~2 bytes | ~4 bytes |
| Connection | Long-lived | Long-lived | Long-lived | Short-lived |
| QoS levels | 3 | 3 | 3 | 3 |
| Topics | Hierarchical | Hierarchical | Hierarchical | Observer pattern |
| Transactions | Supported | Not standard | Not standard | Not standard |
| Portable | Yes (standardized) | Vendor-specific | Limited | Limited |
| Typical use | Enterprise messaging | RabbitMQ classic | IoT/sensors | IoT constrained |
AMQP 1.0 is the most feature-complete and standardized, wire-compatible across implementations.
AMQP 0-9-1 (RabbitMQ classic) has richer features but locks you into a specific implementation.
MQTT targets low-bandwidth, unreliable networks. It is the de facto standard for IoT.
CoAP targets extremely constrained devices like 8-bit microcontrollers, using HTTP-like requests over UDP.
Message Broker Selection Flowchart
Given a new project, here is how to narrow down which broker fits:
graph TD
Start[New messaging project] --> Scale{What's your scale?}
Scale -->|Millions of msgs/day| HighVolume{High throughput?}
HighVolume -->|Yes| KafkaOrArtemis{Area of focus?}
HighVolume -->|No| MediumScale
Scale -->|Thousands of msgs/day| MediumScale[Standard relational DB<br/>or lightweight broker]
KafkaOrArtemis -->|Distributed streaming<br/>log, event sourcing| Kafka[Apache Kafka]
KafkaOrArtemis -->|Enterprise messaging<br/>transactions, AMQP| Artemis[ActiveMQ Artemis]
MediumScale --> NeedsMultiProtocol{Need multi-protocol?}
NeedsMultiProtocol -->|AMQP + MQTT + STOMP| Artemis
NeedsMultiProtocol -->|Just AMQP 0-9-1| RabbitMQ[RabbitMQ]
NeedsMultiProtocol -->|No| CloudManaged{AWS or cloud-native?}
CloudManaged -->|Yes| AWSSQS{AWS-based?}
CloudManaged -->|No| SelfHosted{Self-hosted OK?}
AWSSQS -->|FIFO / exactly-once| SQSFIFO[Amazon SQS FIFO]
AWSSQS -->|Pub/sub, fan-out| SNS[Amazon SNS]
SelfHosted -->|Yes| RabbitMQ
SelfHosted -->|No| AzureEvent{Using Azure?}
AzureEvent -->|Yes| AzureEventHubs[Azure Event Hubs]
AzureEvent -->|No| GCPubsub{GCP?}
GCPubsub -->|Yes| GCPPubSub[Google Cloud Pub/Sub]
GCPubsub -->|No| NATS[NATS]
Quick reference by constraint:
| Constraint | Best choice |
|---|---|
| Exactly-once across systems | SQS FIFO, Kafka (transactions) |
| Message persistence + disk-backed | Kafka (retention), Artemis (journal) |
| AMQP 1.0 native | ActiveMQ Artemis |
| Multi-protocol (AMQP + MQTT + STOMP) | ActiveMQ Artemis |
| Team familiar with RabbitMQ | RabbitMQ |
| Already on AWS | SQS + SNS |
| IoT / extremely lightweight | NATS, MQTT brokers |
| Event sourcing / immutable log | Apache Kafka |
| Highest throughput possible | Apache Kafka, ActiveMQ Artemis |
Pick point-to-point when:
- A message needs processing by exactly one consumer
- You need load leveling (producers faster than consumers)
- Tasks should be processed in order or with fairness
Pick publish-subscribe when:
- Multiple consumers need the same message
- You are broadcasting events to many services
- Consumers are independent and all need to react
Most real systems use both. Order events might go to a topic (for audit logs, notifications, and analytics), while specific order fulfillment tasks go to a queue for the fulfillment worker.
For deeper dives, see our posts on RabbitMQ, Apache Kafka, and AWS SQS/SNS.
Topic-Specific Deep Dives
The sections below go deeper on specific protocols and implementations. Each one covers a particular technology in detail: where it fits, what tradeoffs it makes, and how it compares to alternatives. Jump to the one that matches your stack or your problem.
The three major protocol families covered here are JMS (a Java API, not a wire protocol), AMQP (a wire protocol with rich routing semantics), and MQTT (designed for constrained IoT devices). There is also a section on CloudEvents, a vendor-neutral event format that abstracts away the differences between broker implementations.
JMS: The Java Standard
Java Message Service (JMS) is an API standard for messaging. It defines interfaces, not implementations. You write to the JMS API, and your underlying provider (ActiveMQ, RabbitMQ, IBM MQ) handles the details.
JMS supports both queues and topics:
// Point-to-point
Queue queue = session.createQueue("tasks");
MessageProducer producer = session.createProducer(queue);
producer.send(session.createTextMessage("process this"));
QueueReceiver receiver = session.createReceiver(queue);
Message msg = receiver.receive();
// Publish-subscribe
Topic topic = session.createTopic("events");
MessageProducer producer = session.createProducer(topic);
producer.send(session.createTextMessage("something happened"));
TopicSubscriber subscriber = session.createSubscriber(topic);
Message msg = subscriber.receive();
JMS 2.0 simplified the API and added delivery delays, but the core ideas did not change. The old API required verbose setup: factory, connection, start, session, then finally producer and consumer. JMS 2.0 cut through the boilerplate significantly.
JMS 1.x vs 2.0 API Comparison
// JMS 1.x — verbose setup for every message
ConnectionFactory factory = new ActiveMQConnectionFactory("tcp://localhost:61616");
Connection connection = factory.createConnection();
connection.start();
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Queue queue = session.createQueue("tasks");
MessageProducer producer = session.createProducer(queue);
TextMessage message = session.createTextMessage("process this");
producer.send(message);
session.close();
connection.close();
// JMS 2.0 — @Inject approach uses CDI (Contexts and Dependency Injection)
// Container manages connection, session lifecycle automatically
@Inject
private JMSContext context;
@Inject
private Queue tasksQueue;
public void sendTask(String taskData) {
// One-liner send — no manual session management
context.createProducer().send(tasksQueue, taskData);
}
// Receiving with simplified API
@Inject
private JMSContext context;
public String receiveTask() {
// receive() blocks; receiveBody() returns the typed payload directly
return context.createConsumer(tasksQueue).receiveBody(String.class);
}
The @Inject JMSContext pattern works well in Java EE / Jakarta EE environments. For Spring, JmsTemplate wraps the JMS API with similar convenience.
ActiveMQ Artemis: Modern AMQP
ActiveMQ Artemis is the actively developed successor to classic ActiveMQ. It speaks AMQP 1.0 natively, plus MQTT, STOMP, and HornetQ native protocols, with a different architecture under the ActiveMQ umbrella.
Artemis uses an address-based model rather than the older queue/topic split. Bind addresses to queues with different routing semantics, and you can build almost any pattern you need.
// ActiveMQ Artemis — address-based routing
// Messages sent to "orders.created" address
Address address = session.createAddress("orders.created");
// Queue bound to the address receives all messages
Queue queue = session.createQueue("orders-queue").bind(address);
// Fully Qualified Domain Naming for clustering
// artemis: clustering://artemis01.example.com,5672
Artemis vs RabbitMQ
| Aspect | ActiveMQ Artemis | RabbitMQ |
|---|---|---|
| AMQP version | AMQP 1.0 only | AMQP 0-9-1 (classic), 1.0 via plugin |
| Protocol support | AMQP, MQTT, STOMP, HornetQ, OpenWire | AMQP 0-9-1, MQTT, STOMP |
| Queue model | Address-based (any pattern) | Exchange + binding (classic) |
| Clustering | Master/slave, replicated (Janus) | Standard master/slave |
| Message count | Millions per second | ~50K-100K/second sustained |
| Disk persistence | Journal (append-only, very fast) | Mnesia + transient messages |
| Client languages | Any with AMQP 1.0 client | Erlang (OTP), many language clients |
Artemis has higher raw throughput and the address-based model gives more routing flexibility. RabbitMQ has operational maturity and a larger ecosystem to draw from.
CloudEvents: Vendor-Neutral Event Format
Most messaging systems define their own message envelope format. CloudEvents, a CNCF specification, standardizes how events look across different systems so you are not locked into one vendor’s schema.
{
"specversion": "1.0",
"id": "message-uuid-12345",
"source": "//my-service/orders",
"type": "com.example.order.created",
"subject": "order-789",
"time": "2026-03-26T10:15:30Z",
"datacontenttype": "application/json",
"data": {
"order_id": "789",
"customer_id": "cust-456",
"total": 129.99
},
"extensions": {
"traceparent": "00-abc123-def456-01"
}
}
The source field identifies where the event came from, type uses reverse-DNS naming to describe what happened, and subject pinpoints which entity the event is about. Extensions carry vendor-specific metadata like distributed trace context.
# Publishing a CloudEvent with cloudevents-sdk
from cloudevents.sdk import CloudEvent
# Create a CloudEvent
ce = CloudEvent(
source="//my-service/orders",
type="com.example.order.created",
data={"order_id": "789", "customer_id": "cust-456", "total": 129.99}
)
# Serialize to JSON (HTTP binding)
from cloudevents.sdk.http import to_http
headers, body = to_http(ce)
# headers['Content-Type'] = 'application/cloudevents+json'
Many platforms now produce or consume CloudEvents natively: AWS EventBridge, Azure Event Grid, Google Cloud Events, Knative, and Solace all support it. Using CloudEvents as your internal event format means you can plug into any of these platforms without rewriting event producers or consumers.
If JMS is an API, think of AMQP as a wire protocol—defining how clients and brokers talk over the network, making it language-agnostic.
AMQP’s model has three main pieces:
- Exchange: Takes messages from publishers and routes them based on rules
- Queue: Holds messages until consumers pick them up
- Binding: Tells an exchange which queue to route which messages to
graph LR
Publisher -->|publish| Exchange[Exchange]
Exchange -->|route| Q1[Queue 1]
Exchange -->|route| Q2[Queue 2]
Exchange -->|route| Q3[Queue 3]
The exchange type determines routing behavior:
- Direct: Route to queue matching the routing key exactly
- Fanout: Route to all bound queues
- Topic: Route to queues matching wildcard patterns
AMQP supports both P2P (via direct exchange to single queue) and pub/sub (via fanout or topic exchanges).
MQTT: Lightweight for IoT
MQTT was built for constrained devices and unreliable networks. It dominates IoT where bandwidth is scarce and devices go offline often.
MQTT vocabulary differs from the mainstream:
- Broker instead of server
- Client instead of consumer
- QoS levels instead of delivery guarantees
MQTT QoS levels:
- QoS 0: At most once—fire and forget, no acknowledgment
- QoS 1: At least once—message arrives, consumer acknowledges
- QoS 2: Exactly once—two-phase delivery prevents duplicates
The lightweight design makes MQTT suitable for sensors and actuators that cannot handle the overhead of AMQP or JMS.
When to Use / When Not to Use
When to Use Point-to-Point Messaging
Point-to-point queues fit situations where exactly one worker must handle each unit of work and it does not matter which worker picks it up. The queue acts as a load balancer, routing messages to whichever consumer is available. This matters most when work items are independent and can be processed in any order.
Task distribution is the primary use case. Image resizing, PDF generation, email sending — these are all tasks where any worker instance can handle any task. You do not care which processor runs the job, only that someone does. Point-to-point gives you that for free by routing each message to the next available consumer. If your workers are stateless (as they should be), you can scale the consumer pool up or down without changing anything else.
Load leveling becomes critical when producers generate work faster than any single consumer can process it. The queue sits between producers and consumers and absorbs the burst. If your order service suddenly receives 10,000 orders in a spike but your fulfillment service can only process 500 per minute, the queue holds the backlog and the fulfillment workers drain it at their pace. The order service does not crash, and the fulfillment service does not get overwhelmed. Without the queue, the spike would either lose messages or crash the consumer.
Request/response decoupling applies when the sender does not need to wait for a reply. The sender drops a message in the queue and moves on. Some other service picks it up, processes it, and done. The original sender never blocks. This is useful for operations that take time — sending a password reset email, generating a report, processing a large import. The user does not wait while the work happens; they get notified when it is ready.
Ordered processing is available when you need messages handled in sequence. A single consumer reading from a queue processes messages in FIFO order. If your business logic requires strict ordering — processing stock trades in the order they arrived, or applying database changes in sequence — point-to-point with one consumer gives you that guarantee. The tradeoff is throughput: with one consumer you lose parallelism. If you need ordering and scale, look at Kafka partitioned topics or SQS FIFO queues.
When Not to Use Point-to-Point
Point-to-point breaks down when your problem is one-to-many rather than one-to-one. If one event needs to trigger reactions in multiple independent services, a queue actually makes things harder. You end up creating a separate queue per consumer, managing N queues instead of one topic, and the producer now knows about all N destinations. That re-introduces the coupling you were trying to avoid.
Fan-out requirements are the clearest signal to use pub/sub instead. When the order service needs to notify the warehouse, the billing system, the fraud engine, and the analytics pipeline all at once, a single queue cannot deliver to all of them. You could set up an exchange that routes to multiple queues, but at that point you are rebuilding pub/sub functionality with more complexity. Just use a topic.
Simple notifications — broadcasting a single message to every interested party — also belong in pub/sub. If you find yourself creating the same message content for multiple queues, the queue model is working against you. A topic with N subscribers receiving the same message is cleaner and easier to extend: add a new subscriber without touching the publisher or any existing queues.
Event broadcasting is the same pattern with different vocabulary. When “something happened” and you want every interested service to react, point-to-point requires the producer to know all its consumers and send N copies. Pub/sub lets the producer broadcast once. The downstream services subscribe independently. This matters as your system grows — adding a new consumer does not require a producer change when you use topics.
The underlying question: is the work “someone must handle this once” or “everyone who cares should react to this”? Point-to-point is built for the first case. If you are in the second case, a topic is the right abstraction.
When to Use Publish-Subscribe Messaging
Pub/sub works well when a single fact needs to reach multiple independent consumers and those consumers do not need to know about each other. The producer emits an event and moves on. Whatever happens downstream is not the producer’s concern. That decoupling is the real benefit — services evolve independently, deploy on their own schedules, and fail independently too.
Event broadcasting is the core use case. When order.placed happens and the warehouse needs to reserve inventory, billing needs to charge the card, fraud needs to run a check, and the analytics pipeline needs to record the event — none of that is the order service’s problem to manage. The order service publishes one event and is done. The warehouse, billing, fraud, and analytics services each subscribe to order.placed and handle their own part. Adding a new downstream reaction — say, a Slack notification for high-value orders — does not require changing the order service. You add a new subscriber.
System-wide notifications follow the same pattern. When the deployment pipeline finishes a rollout, ten different teams might want to know: the SRE team wants an incident management alert, the product team wants a dashboard update, the security team wants an audit log entry. The deployment service publishes deployment.completed once. Each subscriber receives it independently. None of those downstream concerns touch the deployment service’s code.
Decoupled microservices benefit from pub/sub when you want services to remain unaware of each other. The inventory service does not need to know that the notification service exists, or that the analytics team just added a new Kafka consumer. It publishes inventory.reserved and the event bus handles distribution. This independence matters as organizations grow: teams can ship changes to their own services without coordinating with other teams, as long as the event schema stays compatible.
Real-time updates work well with pub/sub for pushing the same data to multiple clients or services simultaneously. A live sports score, a stock price, a collaborative document change — all of these need to reach many subscribers at once. The publisher sends the update once and all subscribers receive it. Building this with queues would require a fan-out pattern that gets messy fast.
The practical question: does the producer need to know who is listening? If not, pub/sub fits naturally. If the producer needs to know which consumers received the message, or only one consumer should act on it, use a queue instead.
When Not to Use Publish-Subscribe
- Sequential processing: order matters and only one consumer should handle it
- Request/response: you need a reply from a specific service
- Task queues: work needs assignment to specific available workers
| Failure | Impact | Mitigation |
|---|---|---|
| Broker goes down | Messages cannot be sent or received | Cluster with replication; use durable queues |
| Consumer crash mid-processing | Message lost (auto-ack) or reprocessed | Manual acknowledgments; idempotent processing |
| Network partition | Messages stuck or delayed | Connection recovery; appropriate socket timeout |
| Queue overflow | New messages rejected or old dropped | Max queue length policies; monitor queue depth |
| Message TTL expiration | Unprocessed messages disappear | Appropriate TTL; dead letter queues for failures |
| Duplicate message delivery | Same message processed multiple times | Idempotent consumers; deduplication keys |
| Routing key mismatch | Messages go to wrong queue or nowhere | Consistent naming; dead letter exchanges |
Production Failure Scenarios
Message queue failures take different shapes depending on which component breaks. A broker crash loses in-flight state. A consumer crash mid-ack leaves messages half-processed. A network partition stalls delivery. Each scenario demands a different response, and your architecture should account for all of them.
The table below maps common failure modes to their impact and the mitigation that applies:
| Failure | Impact | Mitigation |
|---|---|---|
| Broker goes down | Messages cannot be sent or received | Cluster with replication; use durable queues |
| Consumer crash mid-processing | Message lost (auto-ack) or reprocessed | Manual acknowledgments; idempotent processing |
| Network partition | Messages stuck or delayed | Connection recovery; appropriate socket timeout |
| Queue overflow | New messages rejected or old dropped | Max queue length policies; monitor queue depth |
| Message TTL expiration | Unprocessed messages disappear | Appropriate TTL; dead letter queues for failures |
| Duplicate message delivery | Same message processed multiple times | Idempotent consumers; deduplication keys |
| Routing key mismatch | Messages go to wrong queue or nowhere | Consistent naming; dead letter exchanges |
Broker goes down is the most severe case. Without clustering and replication, you lose any messages in the broker’s memory. Durable queues write to disk before acknowledging the producer, so messages survive restarts. If your broker does not support persistence, you need to weigh whether in-flight data loss is acceptable for your use case.
Consumer crash mid-processing depends on your acknowledgment mode. With auto-ack, the broker removes the message the moment it delivers it — if the consumer crashes before handling it, the message is gone. Manual ack gives you control: the message stays in the queue until you call ack. If your consumer times out or crashes, the broker redelivers. Pair manual ack with idempotent processing so redelivered messages do not cause duplicate side effects.
Network partitions are nastier than they look. The connection appears healthy to the client library but no data moves. Set socket timeouts shorter than your tolerance for stuck messages, and configure connection recovery to reconnect automatically when the partition heals.
Queue overflow happens when producers outpace consumers for too long. Brokers typically either reject new messages, drop old ones, or spill to disk. Set max queue length policies that match your tolerance. Monitor queue depth and alert before it hits the limit.
Message TTL expiration catches messages that sit too long without being consumed. If a message expires before a slow consumer gets to it, it is gone. Set TTL high enough to account for processing delays, and route expiring messages to a DLQ so you can inspect them later.
Duplicate delivery is the flip side of at-least-once semantics. If a consumer does not ack before crashing, the broker redelivers. Your consumer sees the same message twice. Idempotent processing — deduplication keys, conditional writes — prevents duplicates from causing problems.
Routing key mismatch silently drops messages when the binding between exchange and queue does not match the routing key. This is a configuration problem that can sit unnoticed for days. Consistent naming conventions and dead letter exchanges catch these cases before they pile up in hidden queues.
Common Pitfalls / Anti-Patterns
Pitfall 1: Treating Pub/Sub like a Queue
The mistake here is treating a topic like a queue. In pub/sub, every subscriber to a topic gets a copy of every matching message. If your intent is that exactly one subscriber processes each message, pub/sub is the wrong model. Broadcasting to all subscribers when only one should act wastes processing cycles and can cause race conditions where multiple consumers try to handle the same message inconsistently.
The fix depends on what you actually need. If one subscriber should process each message, use a queue instead. If you need exactly one consumer to handle each message but you want the flexibility of topic-style subscriptions, put a queue in front of each subscriber. The queue acts as the buffer that enforces the “one at a time” semantics while the subscriber reads from its personal queue.
Some systems let you use content-based filtering so only one subscriber acts on a given message. This works if the filtering logic is stable and the number of subscribers is small. But if you find yourself writing filter rules that encode “which consumer should handle this,” you have re-created queue semantics inside pub/sub — cleaner to just use a queue.
Pitfall 2: Ignoring Message Ordering Requirements
Ordering is not free, and most message queue systems do not guarantee it by default. If your business logic requires messages to be processed in a specific sequence — stock trades, database updates, inventory reservations — you need to actively design for it. Default configurations in most brokers give you at-most-once or at-least-once delivery without ordering guarantees.
The classic solution is a single consumer per queue. With one consumer, messages leave the queue in the order they arrived. This is simple and correct, but it sacrifices parallelism. You cannot scale the consumer pool because multiple consumers would compete for messages and destroy ordering.
Kafka-style partitioned topics offer a middle ground. Partition by a key — orderId, userId, accountId — and all events for the same key land in the same partition. That partition is processed by one consumer at a time, so you get ordering per key plus parallelism across partitions. The tradeoff is that cross-partition ordering is not guaranteed, and choosing the wrong partition key can create hot spots.
Before you commit to an ordering strategy, ask whether your consumers can tolerate out-of-order messages with idempotency checks. Most event processing workloads can. If yours cannot, architect for it from the start rather than retrofitting it later.
Pitfall 3: Auto-Acknowledgment Without Idempotency
Auto-ack sounds convenient: the broker removes the message the moment it delivers it to your consumer. If your consumer crashes after receiving but before processing, the message is gone. No retry, no redelivery — just data loss.
The trap is using auto-ack in production systems where message loss is unacceptable. Billing events, order confirmations, inventory deductions — these cannot be silently dropped. If you use auto-ack, your consumer must be idempotent. The broker might redeliver the same message after a network glitch or consumer restart, and your processing logic must handle duplicates gracefully.
The safer path is manual acknowledgment. The broker holds the message until your consumer explicitly calls ack. If the consumer crashes, times out, or rejects the message, the broker redelivers it. You control when a message is considered handled. Combined with idempotent processing — deduplication keys, conditional database writes — manual ack gives you both reliability and safety against duplicates.
If auto-ack is already in your codebase, treat it as a reliability debt. Every auto-ack message that matters is a potential data loss incident waiting to happen.
Pitfall 4: Not Handling Poison Messages
A poison message is a message that your consumer cannot process, no matter how many times it is retried. Bad JSON, a missing dependency, a corrupted payload — whatever the cause, the message keeps failing and the broker keeps redelivering it. With auto-ack, it disappears on first delivery and you lose it entirely. With manual ack and no retry limits, it blocks the queue — every message behind it waits while the broker redelivers the same poison message over and over.
The fix is straightforward: set a maximum delivery attempt count on each queue. When a message exceeds that limit, the broker moves it to a dead letter queue instead of redelivering. The DLQ preserves the original message payload plus metadata about the failure — exception type, error message, retry count, timestamps. Your DLQ consumer can then inspect, fix, and retry or discard.
Without DLQ configuration, poison messages accumulate in the main queue or get dropped silently. Either way, you lose visibility into what is actually failing. Set retry limits, monitor DLQ depth, and have a process for working through DLQ messages before they pile up.
Pitfall 5: Coupling Publishers to Topic Structure
Pub/sub works best when publishers do not know who is listening. If your publisher code references specific subscriber names, routing rules that encode subscriber interests, or topic names that reflect downstream processing steps, changing a subscriber means changing the publisher. That re-introduces the coupling the pattern is supposed to eliminate.
The symptom is fear. Teams stop changing subscribers because they worry about breaking publishers. Publishers accumulate knowledge about downstream consumers that should not be their concern. Over time, the topic structure becomes brittle and hard to change.
The solution is to keep topic design stable and subscriber-agnostic. Name topics around what happened — order.placed, payment.processed, user.created — not around who cares. Subscribers filter and route based on their own needs. If a new service needs order.placed events, it subscribes. The publisher does not change. If a service no longer needs a message type, it unsubscribes. The publisher still broadcasts the same event.
Content-based filtering gives subscribers control without publisher involvement. The publisher sends the full event. Subscribers decide what to act on. This keeps the two sides truly decoupled.
Pitfall 6: Using a Single Queue for Multiple Concerns
Putting every message type in one queue feels simpler at first. One queue to monitor, one DLQ to check, one consumer thread to manage. But as the system grows, this single-queue approach becomes a burden.
The problem is that different message types have different processing requirements. Some need ordering. Some can be processed in parallel. Some fail rarely, others fail often. When all message types share a queue, a slow consumer for one type backpressure the whole queue. A poison message for one type blocks delivery of unrelated types. Consumer logic becomes a tangled switch statement instead of focused handlers.
Separate queues per message type solve this. Each type gets its own consumer pool, its own retry configuration, its own DLQ. A spike in order messages does not affect delivery message processing. You can tune prefetch, concurrency, and retry behavior independently per type. Monitoring is clearer: queue depth spikes tell you exactly which message type is experiencing problems.
The tradeoff is operational complexity — more queues to monitor and configure. But that cost pays for itself in isolation. When a billing message spikes, you see it in the billing queue depth, not in a combined queue where it drowns out other signals.
Interview Questions
Expected answer points:
- Point-to-point: Each message goes to exactly one consumer; queue holds messages until processed
- Publish-subscribe: Messages published to a topic; all subscribers receive a copy
- P2P is for task distribution, pub/sub is for event broadcasting
- P2P provides load leveling; pub/sub provides fan-out to multiple consumers
Expected answer points:
- QoS 0 (At most once): Fire and forget, no acknowledgment, messages may be lost
- QoS 1 (At least once): Consumer acknowledges, messages may be duplicated but not lost
- QoS 2 (Exactly once): Two-phase delivery prevents both loss and duplicates
- QoS 0 for high-frequency, losable data like sensor readings; QoS 1 for general IoT; QoS 2 for critical commands
Expected answer points:
- A DLQ captures messages that fail repeatedly and cannot be processed
- Prevents poison messages from blocking the main queue indefinitely
- Stores original message plus failure metadata (exception type, retry count, timestamps)
- Enables monitoring and manual intervention for failed messages
- Configured via max delivery attempts and dead letter address settings
Expected answer points:
- Prefetch limits control how many messages a consumer has in-flight at once
- Flow control pauses producers when broker resources are low
- Consumer scaling adds more instances to process messages faster
- Circuit breakers pause consumption when error rates spike
- Kafka consumer lag indicates how far behind consumers are; scaling partitions helps
Expected answer points:
- JMS is a Java API specification; it defines interfaces not implementations
- AMQP is a wire protocol defining how clients and brokers communicate over the network
- JMS is Java-centric; AMQP is language-agnostic
- JMS providers (ActiveMQ, HornetQ) implement the API; AMQP clients work across implementations
- JMS 2.0 simplified the API with CDI injection; older 1.x required verbose boilerplate
Expected answer points:
- QoS 2 in MQTT provides exactly-once via two-phase protocol (prepare then commit)
- Idempotent consumers: store processed message IDs and skip duplicates
- Deduplication keys: use unique message IDs with short TTL in Redis or database
- Transactional outbox pattern: write to database and message queue atomically
- Trade-off: exactly-once is expensive; most systems use at-least-once with idempotent consumers
Expected answer points:
- Kafka provides ordering within a partition, not across the entire topic
- Partitioning by a key (e.g., orderId) ensures all events for the same entity go to the same partition
- Single-partition topics give you FIFO ordering but no parallelism
- Traditional queues with a single consumer also provide FIFO; parallelism requires careful design
- Consumer lag monitoring is critical for Kafka ordering guarantees
Expected answer points:
- Artemis speaks AMQP 1.0 natively; RabbitMQ uses AMQP 0-9-1 (classic) plus 1.0 via plugin
- Artemis has address-based routing; RabbitMQ uses exchange + binding model
- Artemis handles millions of messages per second; RabbitMQ sustains ~50K-100K/second
- Artemis uses append-only journal for persistence; RabbitMQ uses Mnesia
- Artemis supports MQTT, STOMP, HornetQ, OpenWire; RabbitMQ supports AMQP, MQTT, STOMP
Expected answer points:
- CloudEvents is a CNCF specification for vendor-neutral event format
- Standardizes event structure across different systems and cloud providers
- Includes specversion, id, source, type, subject, time, datacontenttype, and data fields
- Extensions field carries vendor-specific metadata like distributed trace context
- Supported natively by AWS EventBridge, Azure Event Grid, Google Cloud Events, Knative, Solace
Expected answer points:
- SQS for simple point-to-point queues with managed infrastructure; FIFO option for ordering
- SNS for pub/sub fan-out to multiple subscribers; pairs well with SQS for queue-backed subscribers
- Kafka for high-throughput event streaming, event sourcing, or immutable logs
- Consider: throughput needs, ordering requirements, protocol support, operational complexity
- Hybrid approach: SNS for pub/sub notifications, SQS for task queues, Kafka for event streaming
Expected answer points:
- Queue-based: messages go to one consumer that pulls from the queue; load balancing across consumers possible
- Topic-based: messages published to a topic broadcast to all subscribers; each gets a copy
- Queues use direct addressing (consumer pulls); topics use subscription addressing (broker pushes to subscribers)
- Topic models enable fan-out patterns; queue models enable work distribution patterns
- Most messaging systems support both models (RabbitMQ exchanges, JMS queues/topics, SQS/SNS)
Expected answer points:
- Consumers acknowledge messages after successful processing to tell the broker the message was handled
- Auto-ack: broker removes message immediately on delivery; risky if consumer crashes before processing
- Manual ack: consumer controls when to acknowledge; enables retry on failure, exactly-once semantics
- Acknowledgment latency affects throughput; batching acks can improve performance
- Without proper acknowledgments, messages can be lost (auto-ack + crash) or duplicated (no ack + redelivery)
Expected answer points:
- Problem: writing to a database and publishing a message atomically is hard without distributed transactions
- Solution: write both the business record AND an outbox record in the same database transaction
- A separate process polls the outbox table and publishes messages to the broker
- Guarantees at-least-once delivery since the outbox record persists the intent to publish
- Used in CDC (change data capture) and event sourcing patterns to avoid dual-writes
Expected answer points:
- Single consumer per queue: messages processed in FIFO order (simplest case)
- Partitioned topics (Kafka): messages with same partition key maintain ordering; different partitions can be processed in parallel
- Sequence numbers: embed incrementing sequence in each message; consumer tracks highest seen and skips out-of-order
- Sharding by entity: all events for the same entity (e.g., orderId) go to the same consumer or partition
- Trade-off: strict ordering reduces parallelism; most systems can tolerate eventual ordering with idempotency
Expected answer points:
- Synchronous: caller blocks until response; simple mental model, easy debugging, tight coupling
- Asynchronous: caller sends and forgets; better fault tolerance, backpressure handling, scalability
- Synchronous suffers from cascading failures; async decouples producer from consumer failures
- Async adds complexity: message ordering, delivery guarantees, idempotency, DLQ handling
- Hybrid: use async for business events, synchronous for user-facing requests that need immediate response
Expected answer points:
- Prefetch limits unacknowledged messages in-flight to a single consumer
- Low prefetch (e.g., 1): low memory usage, higher latency, better load balancing across slow consumers
- High prefetch (e.g., 100+): batch processing efficiency, higher throughput, more memory per consumer
- Prefetch 0: consumer receives all available messages at once (RabbitMQ behavior differs)
- Kafka uses `fetch.min.bytes` and `max.poll.records` instead of prefetch; controls how much data is returned per poll
Expected answer points:
- Persistence: messages are written to disk (or journal) before acknowledgment; survives broker restart
- Durability: queue/topic survives broker restart (depends on persistence + clustering + replication)
- Non-persistent messages may be lost on broker crash; persistent messages are written to disk
- Kafka: uses segment files and configurable `log.retention.hours`; messages persist until retention expires
- Trade-off: persistence adds latency (disk I/O); use for critical messages, disable for high-throughput low-value data
Expected answer points:
- Producer sends message with a `replyTo` header specifying a temporary response queue
- Consumer processes message and sends reply to the `replyTo` queue with correlation ID
- Producer consumes from response queue, matching `correlation_id` to original request
- TTL on response queue cleans up unanswered requests; timeout on producer side handles lost replies
- Pattern is async by nature; caller must handle waiting for response without blocking the thread
Expected answer points:
- Consumer group: set of consumers that share the work; each partition goes to exactly one consumer in the group
- If a consumer crashes, its partitions are rebalanced to other group members automatically
- Traditional queues: only one consumer receives each message (competing consumers pattern)
- Kafka: multiple consumer groups can each read the same messages independently (pub/sub semantics)
- Number of consumers in a group should not exceed number of partitions for maximum parallelism
Expected answer points:
- Use queue as a buffer between producers and consumers; queue absorbs the spike, consumers process at their pace
- Set appropriate queue capacity and overflow policies (reject new messages, overflow to disk, etc.)
- Implement consumer scaling: add more consumers or partitions to process backlog faster
- Use message throttling: slow down producers when queue depth exceeds threshold
- Monitor queue depth, consumer lag, and error rates; set up alerts for anomalies
- Design idempotent consumers so duplicate processing during catch-up is safe
Further Reading
- RabbitMQ - Deep dive into RabbitMQ exchanges, bindings, and practical patterns
- Apache Kafka - Event streaming, partitions, and consumer groups
- AWS SQS/SNS - Managed cloud messaging services
- Event-Driven Architecture - Patterns for building loosely coupled systems
Conclusion
Quick Recap Checklist
- Point-to-point (P2P): each message goes to exactly one consumer — use for task distribution, load leveling
- Publish-subscribe (pub/sub): each message goes to all subscribers — use for event broadcasting, fan-out
- At-least-once delivery with idempotent consumers is the practical choice for most systems
- Dead letter queues (DLQs) handle poison messages; configure retry limits
- Prefetch limits prevent slow consumers from being overwhelmed
- Circuit breakers pause consumption when error rates spike
- Message ordering requires single consumer (FIFO) or partitioned topics (Kafka)
- JMS is a Java API; AMQP is a wire protocol; MQTT is for constrained IoT devices
- CloudEvents provides vendor-neutral event format for cross-platform interoperability
- ActiveMQ Artemis: AMQP 1.0, millions/sec throughput, address-based routing
- RabbitMQ: AMQP 0-9-1, ~50-100K/sec, exchange+binding model, mature ecosystem
- Kafka: high-throughput event streaming, immutable log, consumer group parallelism
- SQS/SNS: managed cloud queues; FIFO for ordering, pub/sub via SNS fan-out
- Idempotent consumers prevent duplicate processing with at-least-once delivery
- Transactional outbox pattern avoids dual-write inconsistency in database+queue scenarios
Key Points
- Point-to-point delivers each message to exactly one consumer; publish-subscribe delivers to all subscribers
- P2P works well for task distribution and work queues; pub/sub works well for event broadcasting
- JMS is a Java API standard; AMQP is a wire protocol; MQTT is lightweight for IoT
- Queues give you persistence and load leveling; topics give you fan-out and flexibility
- Design for at-least-once delivery with idempotent consumers
Pre-Deployment Checklist
- [ ] Queue depth monitoring configured
- [ ] Dead letter queues configured for failed messages
- [ ] Manual acknowledgment implemented (preferred over auto-ack)
- [ ] Idempotent message processing implemented
- [ ] Retry limits set with exponential backoff
- [ ] TLS/encryption enabled for client connections
- [ ] Consumer group scaling strategy defined
- [ ] Message TTL configured appropriately
- [ ] Alert thresholds set for queue depth and error rates
- [ ] Schema validation in place for incoming messages
- [ ] Correlation ID propagation implemented for distributed tracing
Observability Checklist
A message queue system without observability is a black box. You cannot tell if messages are backing up, consumers are failing, or throughput is dropping. Here are the metrics, logs, and alerts you need to keep the system visible.
Metrics to Monitor
- Queue depth: number of messages waiting to be processed
- Consumer lag: time between message publication and consumption
- Message throughput: messages published or consumed per second
- Error rate: failed message processing attempts
- Acknowledgment latency: time taken to acknowledge messages
- Connection count: active producers and consumers
Logs to Capture
- Message publish events with routing keys and timestamps
- Consumer acknowledgment and rejection events
- Dead letter queue arrivals with failure reasons
- Connection open and close events
- Retry attempts with attempt counts
Alerts to Configure
- Queue depth exceeds threshold, indicating burst traffic or consumer failure
- Consumer lag exceeds your SLA threshold
- High error rate on message processing
- Dead letter queue accumulating messages
- Broker connection failures
- Consumer disconnection events
Security Checklist
- Authentication: SASL or TLS client authentication for brokers
- Authorization: Queue or topic-level access controls; principle of least privilege
- Encryption in transit: TLS for all client connections
- Encryption at rest: disk encryption for message persistence
- Message validation: validate message schemas before processing
- Input sanitization: sanitize routing keys and message content to prevent injection
- Audit logging: log all administrative operations on queues and topics
- Network segmentation: place brokers in private networks; restrict access via firewalls
Category
Related Posts
Publish/Subscribe Patterns: Topics, Subscriptions, Filtering
Learn publish-subscribe messaging patterns: topic hierarchies, subscription management, message filtering, fan-out, and dead letter queues.
CQRS Pattern
Separate read and write models. Command vs query models, eventual consistency implications, event sourcing integration, and when CQRS makes sense.
Event Sourcing
Storing state changes as immutable events. Event store implementation, event replay, schema evolution, and comparison with traditional CRUD approaches.