Sang's Blog

Message queues, delivery ≠ processing

Message queues guarantee delivery, but delivery is not the same as processing. The queue ensures the message arrives; the application must ensure it is processed correctly.

Service A sends a message. The queue holds it. Service B consumes it, processes it, and acknowledges it. The system works—until Service B crashes after processing but before acknowledging. The queue redelivers the message. Service B processes it again. Now there is duplicate data in the database. Messages arrive out of order. The consumer crashes mid-processing, leaving partial state.

The queue was supposed to simplify things, but now the consumer has more complexity than if it had just made a synchronous HTTP call. The queue guarantees that a message is delivered at least once. It cannot guarantee that the message is processed exactly once. That guarantee is the responsibility of the application, not the queue.

Delivery semantics define what the queue promises. At-most-once means messages might be lost. At-least-once means messages might be duplicated. Exactly-once means each message is processed exactly once. Kafka offers exactly-once semantics with its transactional API. RabbitMQ offers at-most-once or at-least-once, but not exactly-once. But even with exactly-once delivery, the consumer must be idempotent. The queue guarantees it will deliver the message once, but if the consumer crashes after processing and before committing, the message will be redelivered. The application must handle this.

This is not a bug in Kafka or RabbitMQ. It is a fundamental property of distributed systems. The network is unreliable. Messages can be lost, duplicated, or reordered. A message queue abstracts the network, but it cannot eliminate the uncertainty. It can only make the uncertainty explicit through delivery semantics. The application must then handle the uncertainty that the queue exposes.

The practical implication is that message queues do not decouple services as much as people think. The services are still coupled through the message schema, the queue naming, the delivery guarantees. If Service A changes the message format, Service B breaks. If Service A expects exactly-once processing but Service B is not idempotent, a contract violation exists. The coupling is asynchronous instead of synchronous, but it is still coupling.

This is why choosing between Kafka and RabbitMQ is not just a technical decision; it is a decision about which problems need to be handled. Kafka is designed for high-throughput, ordered, durable message streams. It is good for event sourcing, log aggregation, and streaming analytics. RabbitMQ is designed for flexible routing, complex topologies, and traditional task queues. It is good for work distribution, notifications, and request-reply patterns. But both require the application to handle the distributed systems problems that the queue exposes.

The trade-off is between network complexity and application complexity. Without a message queue, synchronous communication exists. The caller waits for the response. If the callee is down, the call fails. The complexity is in handling timeouts, retries, and failures at the network layer. With a message queue, asynchronous communication exists. The producer does not wait for the consumer. The complexity is in handling duplicates, ordering, and partial failures at the application layer. Complexity is not being eliminated; the choice is where it lives.

A message queue solves a network problem: services need to communicate without blocking. But it does not solve application problems: reliable processing, exactly-once semantics, complex workflows. Those problems live in the application, and that is where the real complexity lives.

← Prev Post Next Post →