RabbitMQ (AMQP mostly) and NATS /NATS Streaming comparison

From a (very) high level point-of-view, both products solve the same problem — getting our message bytes from producer to consumer, what allows our architectures to avoid coupling. Let’s take a look at are the similarities and differences provided by these two solutions.

RabbitMQ server allows us to send and receive messages using AMQP 0.9.1 protocol. Apart from that, there are other protocols supported as well, like STOMP. AMQP’s richness allows the users to model multiple communication patterns, using the entities such as exchanges and queues.

For NATS we have NATS Server and NATS Streaming Server. Basically the first one provides an ability to route messages from producers to consumers, while the latter builds upon the first one to enrich it with features such as durability.

Message persistence in RabbitMQ: RabbitMQ provides multiple way of configuring reliability and persistence settings:

  • consumer acknowledgements — used to ensure that the message has reached the intended consumer (what can be performed manually by our code or automatically),
  • transaction support for producer publish and consumer acknowledge methods (guaranteed to be atomic only when a single queue is used),
  • publisher confirms — provided as a faster alternative to transactions, these mechanisms allow us to confirm publishing of multiple messages at once (compared to having a commit per message in transactions),
  • message delivery mode — controls whether the messages stored in RabbitMQ node should be kept in memory or need to be written to disk, what results in different behaviour in case of unexpected broker crash,
  • queue durability — whether the queue’s metadata is stored on disk or in memory (with the same implications as above),
  • queue-level and message-level TTL — what controls when they are purged by the server (additionally RabbitMQ provides dead letter queues that allow us to capture these messages).

Queues provided by RabbitMQ are queues in ordinary sense, messages that have been acknowledged by consumers disappear, and are not available for re-reading. If we want to make the same payload available to multiple consumers, we need to make sure it is going to be delivered to multiple queues, for example by using fanout exchanges.
A queue can be made exclusive, i.e. it is related to a single connection and gets destroyed when that connection is closed.

Last but not least, it is important to remember that RabbitMQ can be running in clustered mode, with queue reliability increased by using (newer) Raft-backed quorum or (older) replicated queues.

Message persistence in NATS: NATS server provides at-most-once delivery semantics and no message persistence (serves the role of distribution layer only), while NATS Streaming provides at-least-once delivery semantics and persistence.

In both cases, there are no queues as “traditionally” understood as in messaging world, instead the queues are a mechanism to distribute messages between all consumers using the same queue group (similar to RabbitMQ’s consumer load balancing). Without using the queues, all consumers subscribing to a given subject are going to receive a message.
As NATS does not provide any persistence, if there are no consumers present for a given subject, then the message is going to be lost.

In case of NATS Streaming solution, the messages sent to channels are persisted in the message log, and are removed according to time-, size-, and count-based policies (this is somewhat similar to how Apache Kafka operates with its retention policies).

The consumers in NATS Streaming can be either regular or durable. This somewhat analogous to having a single AMQP consumer for an exclusive or durable queue.

Complex topologies: Out of the box, AMQP allows us to create very complex topologies — we have multiple exchange types (direct, fanout, topic, headers). However, many of these communication patterns could be also approximated with NATS:

It should be remembered that NATS Streaming (compared to ordinary NATS) does not provide support for wildcards in channel names.

Also, NATS does not provide anything resembling exchange-to-exchange binding (what would mean that for some scenarios we are going to have to publish the same message multiple times instead of publishing once to a single exchange — this could be somehow mitigated by wildcard subscription in “pure” NATS).

Headers: As implied above in topologies pat, RabbitMQ messages can carry additional headers apart from the main payload body. There’s no such capability in NATS, however given the omnipresence of solutions like Protobuf it should be of no problem for developer to roll out their own headers into the byte payload being sent.

Dead-letter in RabbitMQ: RabbitMQ provides a custom AMQP extension, that allows us to set up dead-letter exchanges. They allow us to configure additional message sinks when message could not be consumed, or are rejected, or expire due to TTL threshold. This might allow us to capture payloads that took too long to process or are otherwise unacceptable by our business logic.

Java client for RabbitMQ exposes the AMQP capabilities through the Channel objects. Also, the Connection & ConnectionFactory objects provide us with additional features such detecting connection blocks or recreating AMQP topology (exchanges, queues) when reconnecting. The consumption can be asynchronous (we receive messages through listener provided in basic.consume calls), or synchronous consumption (via basic.get). Also, RMQ client is capable of purging queues.

An additional capability of RabbitMQ is the server’s management HTTP API, that allows us to quickly check the state of the server/cluster (e.g. configuration of exchanges/queues, message rates). Accessing this information in most cases also possible through command-line tools.

Java client for NATS is extremely simple to use, as NATS protocol does not introduce as many concepts as AMQP.

Ordinary NATS connection supports both synchronous and asynchronous access to messages. With the streaming connection, we are only capable of creating asynchronous subscriptions with the subscribe methods. In case of connection loss, the library is capable of recovering the connection on its own (however, there are still some issues while managing connections).

RPC: In general, remote procedure calls can be performed using two queue-like entities, one used to deliver the request, and the other used to carry the response. Both RabbitMQ and NATS support this model.

RabbitMQ uses reply queue attribute and correlation ids as a mechanism for sending responses. Also, there is an additional extension Direct reply-to, that allows us to get rid of the response queue, in favour of the response message being routed immediately to requester’s connection.

In NATS, request-reply is achieved with unique subjects called inboxes. The name of reply queue is accessed by Message’s getReplyTo method.

Server deployment for RabbitMQ: apart from running as a single node, the server can be deployed in distributed fashion:

  • clustering — where exchange and queue metadata replicated across the cluster, and clients can consume messages from the whole cluster transparently; the messages are replicated depending on clustering configuration;
  • federation and shovel plugin — both of which allow for moving messages from broker to broker without replicating the whole topology and without the full clustering overhead.

When it comes to monitoring RabbitMQ, we are provided with command-line tools and REST API. Custom server behaviour can be achieved with adopting plugins.

Server deployment for NATS:

NATS servers can be deployed in clustered mode, allowing for higher resiliency and throughput. An additional constraint is added to message transfer is limited to a single hop, what might have implications for clients connecting only to single nodes.
The cluster functionality can be also extended with additional features such as gateways (that allow us to connect multiple NATS clusters to form a messaging mesh) or leaf nodes (that check local policies for client connections before transferring messages to/from upstream clusters).

NATS Streaming Server uses Raft-backed replication to provide increased reliability and resilience. As an alternative, the server can use partitioning to have the channels distributed across multiple instances, effectively splitting the load.
The store that keeps the messages is configurable, we can use in-memory, file-based, or SQL-backed stores. In case of stores when running in clustered mode, the store needs to be persistent.

Final thoughts

Comparing these two technologies is a bit like comparing apples to oranges — NATS on its own provides a simpler product that claims to provide excellent performance, while with RabbitMQ we get much larger feature coverage and ability to put some part of application logic into the middleware. The final choice which technology to use (and why not both) depends on the requirements provided to us from the above layers — e.g. the user UI actions are going to impose a different set of requirements than, let’s say, critical business operations.

A trivial messaging usecase where we have a queue read by (possibly multiple) consumers would be easy to model with both technologies:

  • with AMQP, we’d have a single exchange bound to a single queue,
  • with NATS, we’d have a single subject, and all consumers would belong to that queue group.

If we wanted to model pub-sub behaviour, where everyone gets a message:

  • with AMQP, we’d have a single (fanout) exchange bound to multiple (potentially exclusive) queues — each consumer using its own queue.
  • with NATS, we’d keep a single subject, but this time we’d not use a queue group.

Software developer, Java mostly, C & C++ sometimes (https://www.linkedin.com/in/adam-kotwasinski/)