{"id":9656,"date":"2026-10-12T13:33:37","date_gmt":"2026-10-12T12:33:37","guid":{"rendered":"https:\/\/upcloud.com\/global\/?post_type=tutorial&#038;p=9656"},"modified":"2026-10-12T13:33:37","modified_gmt":"2026-10-12T12:33:37","slug":"rabbitmq-monitoring-upcloud","status":"publish","type":"tutorial","link":"https:\/\/upcloud.com\/global\/resources\/tutorials\/rabbitmq-monitoring-upcloud\/","title":{"rendered":"Part 4: RabbitMQ Monitoring, Scaling, and Operations on UpCloud"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">The <a href=\"https:\/\/upcloud.com\/global\/resources\/tutorials\/rabbitmq-cluster-setup-upcloud\/\"><strong>RabbitMQ cluster setup<\/strong><\/a> from Part 3 passes its initial tests. Nodes join, quorum queues replicate, and elections happen when a leader goes down. What comes next is operating that cluster for months. Most RabbitMQ problems won\u2019t show up during setup. They will appear under real workloads, such as when a consumer falls behind, when disk fills faster than expected, or when a node needs to be replaced.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This part covers the operational aspects of running a three-node RabbitMQ cluster on UpCloud, with focusing on RabbitMQ monitoring, load balancing, and scaling. We\u2019ll route client traffic through the load balancer, write clients that handle failure correctly, read the metrics that matter, and work through the failure modes and maintenance tasks we\u2019ll encounter in practice.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Traffic Routing with UpCloud Load Balancer<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">AMQP uses long-lived, stateful TCP connections. A typical application opens one AMQP connection, multiplexes several channels over it, and keeps that connection open for the lifetime of the process. This is meaningfully different from HTTP, where a load balancer can distribute individual requests across backends because each request uses a short-lived connection.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For AMQP, the load balancer\u2019s primary job is failover routing. It provides clients with a single, stable endpoint and stops directing new connections to unhealthy nodes. Once a client connects to a broker node, it remains connected to that node for the lifetime of the connection. The load balancer\u2019s role ends at initial connection placement and failover.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Setting up the UpCloud Load Balancer<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">For this RabbitMQ load balancing setup, create an <a href=\"https:\/\/upcloud.com\/global\/products\/load-balancer\/\">UpCloud Load Balancer<\/a> in the same region as the RabbitMQ nodes. Configure a backend for each RabbitMQ node using its private SDN IP address and port 5672. The load balancer should use the private SDN network to reach the backends, not the public network.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For the health check, use an HTTP check against the RabbitMQ management API\u2019s alarm endpoint on port 15672:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">GET \/api\/health\/checks\/alarms<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This endpoint returns HTTP 200 when the node has no active alarms and HTTP 503 when memory or disk alarms are active. A node with an active alarm is still technically running, but it\u2019s blocking publishers and shouldn\u2019t receive new client connections. Using this endpoint as the health check means the load balancer routes traffic away from nodes under pressure, not just nodes that are completely down.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Configure the health check with a short interval (10-15 seconds) and a low threshold for marking nodes unhealthy (2-3 consecutive failures). RabbitMQ alarm states change quickly, and we want the load balancer to stop routing to an alarmed node before a queue of blocked publishers builds up.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The health check needs credentials. Create a dedicated monitoring user with the monitoring management tag and no other permissions:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">rabbitmqctl add_user monitor &#8216;choose-a-monitoring-password&#8217;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">rabbitmqctl set_user_tags monitor monitoring<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The monitoring tag grants read-only access to the management API, which is enough for health checks.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Separating AMQP and management traffic<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Do not use the same load balancer rule for AMQP client traffic and management UI access. AMQP traffic (port 5672 or 5671 with TLS) belongs behind the load balancer. The management UI (port 15672) should be accessed directly via individual node IPs or a separate restricted route, not through the same load balancer that our application clients use.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The reason is operational clarity. During an incident, we want direct access to each node\u2019s management API without going through a load balancer that may be routing around that node. If we only have load-balanced management access, we may not be able to inspect the node that is causing the problem.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">What the load balancer doesn\u2019t fix<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The load balancer doesn\u2019t make queues available when they don\u2019t have quorum. If two of three nodes are down, the load balancer can route a client connection to the one surviving node, but quorum queues on that node won\u2019t accept writes because a majority of replicas isn\u2019t available. The client will connect successfully and then receive errors when it tries to publish. Quorum is a separate guarantee from connectivity.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Client-Side Reliability<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Our cluster can handle broker-level node failures. What happens to our application during those failures depends almost entirely on how we write the clients. connection retry logic, publisher confirms, and redelivery handling are all application-level responsibilities.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Connection retries and backoff<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">AMQP connections close when a broker node restarts, a network partition occurs, or the load balancer routes the client to a different backend after a health check failure. The application must detect the closure and attempt to reconnect.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Most AMQP client libraries support automatic connection recovery, but it\u2019s often disabled by default or not configured with sensible backoff. When a broker fails and then recovers, every client connected to it will try to reconnect at roughly the same time. Without backoff and jitter, the reconnect storm can overwhelm the recovering node.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We should configure automatic connection recovery with exponential backoff. A reasonable starting interval is 2-5 seconds, increasing up to 30-60 seconds with randomized jitter:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">import pika<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">import time<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">import random<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">def connect_with_backoff(host, credentials, max_attempts=10):<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;delay = 2<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;for attempt in range(max_attempts):<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;try:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;parameters = pika.ConnectionParameters(<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;host=host,<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;credentials=credentials,<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;heartbeat=60,<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;blocked_connection_timeout=300<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;return pika.BlockingConnection(parameters)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;except pika.exceptions.AMQPConnectionError:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if attempt == max_attempts &#8211; 1:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;raise<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;jitter = random.uniform(0, delay * 0.2)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;time.sleep(delay + jitter)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;delay = min(delay * 2, 60)<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Publisher confirms<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Publisher confirms are the only reliable mechanism for knowing that a message has been durably committed by the broker. Without them, publishing is fire-and-forget. A message in flight when the broker crashes is lost with no signal to the producer.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We should enable confirm mode on a channel and handle acks and nacks:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">unconfirmed = {}<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">next_seq = 1 # broker assigns sequence numbers starting at 1, incrementing per publish<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">def on_delivery_confirmed(frame):<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;delivery_tag = frame.method.delivery_tag<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;multiple = frame.method.multiple<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;is_nack = frame.method.NAME == &#8216;Basic.Nack&#8217;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;# &#8216;multiple&#8217; means all tags up to and including delivery_tag are confirmed<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;tags = [t for t in list(unconfirmed) if t &lt;= delivery_tag] if multiple else [delivery_tag]<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;for tag in tags:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;msg = unconfirmed.pop(tag, None)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;if is_nack and msg:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;retry_or_dead_letter(msg)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"># Pass the Ack\/Nack callback directly to confirm_delivery (async Channel only)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">channel.confirm_delivery(ack_nack_callback=on_delivery_confirmed)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"># Track the sequence number before publishing; basic_publish returns None<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">global next_seq<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">seq = next_seq<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">next_seq += 1<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">unconfirmed[seq] = message_body<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">channel.basic_publish(<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;exchange=&#8221;,<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;routing_key=&#8217;orders&#8217;,<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;body=message_body,<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;properties=pika.BasicProperties(delivery_mode=2),&nbsp; # persistent<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;mandatory=True<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A nack from the broker doesn\u2019t mean the message was lost. It means the broker couldn\u2019t commit it at that moment, often because of a transient overload or a failing node. The correct response to that is to retry with backoff, not to drop the message.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Publisher confirms have a throughput cost because every write must be acknowledged before the producer can treat it as committed. For workloads where throughput matters, we can publish in batches and wait for confirms on the batch rather than confirming each message individually.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Consumer acknowledgements<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A consumer should acknowledge a message only after it has finished the work associated with that message. If the consumer crashes before acknowledging the message, RabbitMQ redelivers it to another consumer. If the consumer acknowledges early and then crashes, the work is lost.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Let\u2019s consider setting a prefetch count with basic.qos before starting consumers. Without a prefetch limit, RabbitMQ will deliver all available messages to the first consumer that connects, which can overwhelm a slow consumer and cause unacknowledged counts to spike:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">channel.basic_qos(prefetch_count=20)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">def process_message(channel, method, properties, body):<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;try:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;do_work(body)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;channel.basic_ack(delivery_tag=method.delivery_tag)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;except Exception:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;# Reject and do not requeue; route to dead letter exchange<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;channel.basic_nack(delivery_tag=method.delivery_tag, requeue=False)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">channel.basic_consume(queue=&#8217;orders&#8217;, on_message_callback=process_message)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Use requeue=False when a message fails processing and shouldn\u2019t be retried immediately. With requeue=True, a rejected message goes back to the front of the queue and will be immediately redelivered to the same or another consumer. For messages that consistently fail to process, this creates a tight retry loop that can overwhelm the queue and prevent other messages from being delivered. Route failures to a dead letter exchange instead.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Handling unroutable messages<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A message published to an exchange with a routing key that matches no bound queue is silently discarded by default. The mandatory flag changes this: when set, RabbitMQ returns unroutable messages to the producer via a basic.return callback instead of dropping them.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If we can\u2019t always set mandatory on every publish call, configure an alternate exchange on the exchange itself. An alternate exchange receives any message that the primary exchange can\u2019t route:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">rabbitmqadmin -u admin -p yourpassword -V production \\<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">declare exchange name=app.events type=topic durable=true \\<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">arguments='{&#8220;alternate-exchange&#8221;:&#8221;app.unrouted&#8221;}&#8217;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">rabbitmqadmin -u admin -p yourpassword -V production \\<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">declare exchange name=app.unrouted type=fanout durable=true<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">rabbitmqadmin -u admin -p yourpassword -V production \\<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">declare queue name=unrouted-messages durable=true \\<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">arguments='{&#8220;x-queue-type&#8221;:&#8221;quorum&#8221;}&#8217;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">rabbitmqadmin -u admin -p yourpassword -V production \\<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">declare binding source=app.unrouted destination=unrouted-messages routing_key=&#8221;&#8221;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Messages that reach the alternate exchange are usually a symptom of a routing configuration problem, a stale binding, or a producer publishing to the wrong key. Monitoring the depth of the unrouted messages queue gives us visibility into these issues rather than silently discarding the messages.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Idempotency<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">RabbitMQ guarantees at-least-once delivery when the publisher confirms, and consumer acknowledgments are in use. That means a consumer may receive the same message more than once: when a connection drops before an ack is sent, when a consumer crashes mid-processing, or when a message is nacked with requeue. The redelivered flag in the message properties will be set to true for redeliveries. A false value guarantees the message has not been seen before. A true value means it may have been seen before, but it doesn\u2019t guarantee prior processing was completed, so we can\u2019t use it alone to skip work safely.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This is why we should design consumers to handle duplicates safely. A common approach is to use the message\u2019s message_id property as an idempotency key and check whether that ID has already been processed before doing the work. Use a fast-lookup store for this check, such as Redis with a short TTL, rather than a full database write on every check.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">RabbitMQ Scaling Considerations<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A three-node RabbitMQ cluster has a fixed capacity ceiling. When the workload grows beyond what the cluster can handle, we have a few options, and they aren\u2019t all the same.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Signals that the cluster needs more capacity<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Memory alarms activating under normal load (not just during traffic spikes) usually indicate that the memory threshold is too low or that the node genuinely doesn\u2019t have enough RAM for the workload. Check rabbitmqctl status to see the current memory usage and compare it to the threshold we set.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Publisher flow control is the visible symptom of a constrained cluster. It can be triggered by memory pressure, disk pressure, or slow consumers, causing queue buildup. First, isolate which signal is triggering it before deciding on a response. Adding more nodes won\u2019t fix the flow control caused by a consumer that\u2019s too slow to drain a queue.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Persistent queue backlogs, where the messages-ready count grows over hours without recovering, indicate that consumers can\u2019t keep up with the publish rate. This is an application-level problem (not enough consumer capacity) more often than it\u2019s an infrastructure problem.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Why RabbitMQ doesn\u2019t scale horizontally without limits<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Adding more nodes to a RabbitMQ cluster increases total resource capacity but doesn\u2019t linearly increase throughput for individual queues. Each quorum queue has a fixed replication group, and that group\u2019s leader handles all writes for that queue regardless of how many nodes the cluster has.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Erlang\u2019s distribution protocol (which RabbitMQ uses internally) has overhead that grows with the number of nodes. Every node must exchange heartbeats, metadata, and cluster membership information with every other node. The metadata store uses Raft-based quorum replication, meaning every metadata write must be committed by a majority of nodes. With many nodes, metadata operations become a bottleneck.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The RabbitMQ team\u2019s practical guidance is to run 3 or 5 nodes for most production workloads. This number is enough for quorum without significant inter-node overhead. Larger clusters are possible but should be validated against our specific throughput requirements.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Vhosts as a workload isolation mechanism<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Before adding nodes or splitting clusters, consider whether vhosts would help. A vhost is a logical namespace within a single RabbitMQ cluster. Each vhost has its own exchanges, queues, bindings, users, and policies. Applications using different vhosts don\u2019t share queue names or permissions, which is useful for multitenancy or separating workloads with different access controls.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Vhosts share underlying cluster resources. However, they don\u2019t provide memory, CPU, or disk isolation. A workload in one vhost that fills the disk or triggers a memory alarm affects all vhosts on those nodes. This is why we should use vhosts when we need namespace separation and access control isolation, not when we need resource isolation.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">When separate clusters make more sense<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Separate clusters make sense when workloads have meaningfully different requirements, and you want failure domains to be independent. A payment processing queue and a low-priority notification queue should probably be on different clusters, because a disk issue on the payment cluster shouldn\u2019t affect notifications, and vice versa. Running them together saves infrastructure cost but creates a coupling we may regret during an incident.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Separate clusters also make sense when different teams own different workloads and need independent upgrade cadences, configuration ownership, or operational responsibility. A shared cluster that multiple teams depend on tends to slow down maintenance and create coordination overhead.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">RabbitMQ Monitoring: Key Metrics to Watch<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">RabbitMQ exposes a large amount of telemetry. Most of it is useful for debugging specific incidents, but a smaller set of metrics is worth watching continuously.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Here are the metrics that matter for daily operations:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><em>Messages ready<\/em> is the number of messages waiting in a queue to be delivered to a consumer. A queue where messages-ready consistently grows over time has consumers that can\u2019t keep up with the publish rate. A sudden spike followed by recovery is normal. A steady upward trend signals that consumers can\u2019t keep up.<\/li>\n\n\n\n<li><em>Messages unacknowledged<\/em> is how many messages have been delivered to consumers but not yet acknowledged. This number is bounded by our prefetch configuration. If it stays consistently at the prefetch ceiling, we may not have enough consumer concurrency to process the current delivery rate.<\/li>\n\n\n\n<li><em>Memory used per node compared to the memory threshold<\/em> shows how much headroom remains before flow control activates. We want an alert at roughly 80% of the threshold, which gives us time to investigate before publishers start blocking.<\/li>\n\n\n\n<li><em>Disk free on the data volume compared to the alarm threshold<\/em> alerts us well before the alarm value. By the time the alarm activates, publishers are already blocked.<\/li>\n\n\n\n<li><em>Channel count per connection<\/em> reveals misconfigured clients. A connection with hundreds of channels is almost always a client that is opening channels without closing them. Each channel is an Erlang process, and a leak here will drive the Erlang process count up over time.<\/li>\n\n\n\n<li><em>Erlang process count<\/em> is a cluster-level health indicator. A stable cluster with consistent traffic has a roughly stable process count. A process count that grows over hours without corresponding traffic growth suggests a leak, usually from unclosed connections or channels.<\/li>\n\n\n\n<li><em>Connection count<\/em> and <em>connection events<\/em> can surface reconnect storms (sudden spike in new connections after a node restart), client misconfiguration (clients that keep connecting and immediately disconnecting), or unexpected traffic growth.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">We can use a common open-source platform like Prometheus to set up monitoring.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Setting up the Prometheus plugin<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">To send metrics from our RabbitMQ cluster to Prometheus, we need to first enable the rabbitmq_prometheus plugin on all three nodes:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">rabbitmq-plugins enable rabbitmq_prometheus<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The plugin exposes metrics at http:\/\/node-ip:15692\/metrics in Prometheus format. Now, configure the Prometheus scrape job to collect from all three nodes:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">scrape_configs:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&#8211; job_name: rabbitmq<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;static_configs:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#8211; targets:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#8211; rmq1.internal:15692<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#8211; rmq2.internal:15692<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&#8211; rmq3.internal:15692<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&nbsp;&nbsp;&nbsp;&nbsp;metrics_path: \/metrics<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The RabbitMQ team maintains official Grafana dashboard templates at <a href=\"https:\/\/grafana.com\/grafana\/dashboards\/\" target=\"_blank\" rel=\"noopener\">grafana.com\/grafana\/dashboards<\/a>. We can search for \u201cRabbitMQ\u201d to find the current dashboards. The <a href=\"https:\/\/grafana.com\/grafana\/dashboards\/10991-rabbitmq-overview\/\" target=\"_blank\" rel=\"noopener\">\u201cRabbitMQ-Overview\u201d dashboard (dashboard ID 10991)<\/a> is a good starting point. It covers queue depths, message rates, node resource usage, and connection counts in one view.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Make sure to add alert rules to our monitoring system for:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>memory usage above 80% of threshold<\/li>\n\n\n\n<li>disk free below 2x the alarm threshold<\/li>\n\n\n\n<li>any active alarms<\/li>\n\n\n\n<li>queue depth growing consistently over 15 minutes<\/li>\n\n\n\n<li>Erlang process count growing unexpectedly, etc.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Common Failure Modes<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Here are a few common issues we might run into when using RabbitMQ in production.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Node crash<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">When a RabbitMQ node stops unexpectedly, the cluster detects the failure through missed heartbeats. This takes a few seconds. During that window, clients connected to the failed node see their connections close, and quorum queues with a leader on that node pause while an election happens.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">From a healthy node, we can check what the cluster sees:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">rabbitmqctl cluster_status<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The failed node should appear in \u201cDisk Nodes\u201d but not \u201cRunning Nodes\u201d. We can check whether any queue is missing quorum:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">rabbitmqctl list_queues -p production name leader members online<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A queue where online shows two members still has quorum and will elect a new leader if the old leader was lost. A queue with only one online member can\u2019t elect a new leader and won\u2019t accept writes until a second member returns.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Once the node comes back online and rejoins, we can verify the cluster is fully healthy:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">rabbitmqctl cluster_status<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">rabbitmqctl list_queues -p production name leader members<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">With this, we can confirm that all three nodes are in \u201cRunning Nodes\u201d.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">It is also important to note that queue leaders do not redistribute automatically after a node recovers. The node that won the election during the outage retains leadership. If we want to rebalance leaders across nodes, we can use rabbitmq-queues to rebalance all after the cluster is fully healthy.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Disk full<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">RabbitMQ activates the disk alarm when free space on the data volume drops below the configured threshold. Once the alarm is active, all publishers on that node block. They don\u2019t receive errors. Their publish calls simply don\u2019t return until the alarm clears. From the application\u2019s perspective, publishing appears to hang.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Consumers aren\u2019t blocked by a disk alarm and can continue draining queues. If we can increase the rate of consumption to reduce queue depth, that frees disk space and may clear the alarm on its own.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For immediate relief, we can <a href=\"https:\/\/upcloud.com\/global\/docs\/guides\/increasing-storage-size\">expand the MaxIOPS volume through UpCloud Hub<\/a>. UpCloud lets us resize volumes without stopping the server. Once we\u2019ve increased it, we can resize the filesystem to allow our server to use the new space:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">resize2fs \/dev\/vdb<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Then, we can check that the alarm cleared after the resize:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">rabbitmqctl status | grep -A5 alarms<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">An empty alarms section means the alarm is no longer active and publishers will unblock.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If the disk filled because of unexpected message accumulation (a consumer that went offline and left messages building up), address the root cause before the next time. Add monitoring on queue depth and disk usage so we have warning time before the alarm activates.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Network partition<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Under pause_minority (the setting from <a href=\"https:\/\/upcloud.com\/global\/resources\/tutorials\/rabbitmq-cluster-setup-upcloud\/\">RabbitMQ cluster setup<\/a> in Part 3), the minority node will pause when it loses connectivity to the other two nodes. The paused node stops accepting client connections and stops serving queue traffic.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">To confirm a partition occurred, we can run:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">rabbitmqctl cluster_status<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We will need to look for content under \u201cNetwork Partitions\u201d in the output. The management UI also displays a warning prominently when the partition state is recorded.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">After connectivity is restored, the paused node should restart itself automatically and rejoin the cluster. Once it does, we need to:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Verify all three nodes appear in \u201cRunning Nodes\u201d<\/li>\n\n\n\n<li>Check that queue memberships are intact<\/li>\n\n\n\n<li>Confirm no alarms are active<\/li>\n\n\n\n<li>Run a quick publish\/consume test against each node to confirm the cluster is serving traffic normally.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">A partition under pause_minority doesn\u2019t lose committed messages, because only the majority partition continued accepting writes. The minority node\u2019s view of the world is simply behind while it was paused.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Slow consumers<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Slow consumers cause messages to accumulate in queues. As queue depth grows, RabbitMQ uses more memory. If the depth grows enough to trigger the memory alarm, publishers block even though the problem is on the consumer side.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We can check which queues are accumulating by running:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">rabbitmqctl list_queues -p production name messages messages_ready messages_unacknowledged consumers<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A queue, where messages_ready is large, and consumers is nonzero, but messages_unacknowledged is at the prefetch limit, indicates consumers that are receiving messages but processing them slowly. Adding more consumer instances is the immediate fix. If the processing bottleneck is downstream (a slow database, a slow external API), more consumers won\u2019t help, and we need to address the downstream bottleneck.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A queue with zero consumers and a growing messages_ready count means consumers have gone offline. Check whether the consuming service is running and reconnecting.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Publisher flow control<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Flow control activates when the broker needs to slow down incoming message rates to protect itself. It can be triggered by the memory alarm, the disk alarm, or a per-queue credit flow when a queue can\u2019t keep up with the publish rate to it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Flow control that activates during a known traffic burst and clears within a few minutes as the burst ends is probably transient and acceptable. Flow control that activates regularly under normal load means the cluster doesn\u2019t have enough capacity for the workload.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">We can check what triggered it by running:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">rabbitmqctl status | grep -A10 alarms<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">rabbitmqctl list_queues -p production name messages messages_ready<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If a memory or disk alarm is active, address the alarm. If no alarms are active but flow control is still visible, check whether a specific queue has an unusually large backlog that is causing per-queue flow control.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Backups and Recovery<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">When dealing with production systems, we must always prepare for failure. And failure can be both downtime and lost data. To prevent data loss, we need to have solid backup and recovery plans in place.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">What UpCloud snapshots cover<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">An UpCloud snapshot captures the full disk state of a server volume at a point in time. This includes the OS, RabbitMQ binaries, configuration files, and the RabbitMQ data directory with Raft logs and message store.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A snapshot of a running RabbitMQ node isn\u2019t guaranteed to be consistent because the Raft log may be in the middle of a write operation at the moment of the snapshot. For the most consistent snapshot, we should take it while the node is stopped. If we snapshot a running node, it may still be usable, but recovery may require RabbitMQ to replay the log from its last safe checkpoint.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Snapshots are useful for recovering a complete node (OS, config, and data together) after a server loss, and for fast rollback after a failed upgrade.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Definitions export as configuration backup<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">As in the <a href=\"https:\/\/upcloud.com\/global\/resources\/tutorials\/rabbitmq-setup-upcloud\/\">RabbitMQ setup<\/a> from Part 2, definitions export captures the broker topology: all vhosts, users, permissions, exchanges, queues, bindings, policies, and runtime parameters. It doesn\u2019t capture messages in queues, active connections, or consumer state.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Export definitions from the management API:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">curl -s -u admin:yourpassword \\<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">http:\/\/localhost:15672\/api\/definitions \\<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">&gt; rabbitmq-definitions-$(date +%Y%m%d).json<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Store this file in <a href=\"https:\/\/upcloud.com\/global\/products\/object-storage\/\">UpCloud Object Storage<\/a> on a schedule:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">s3cmd put rabbitmq-definitions-$(date +%Y%m%d).json \\<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">s3:\/\/your-bucket\/rabbitmq\/definitions\/<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A daily export to Object Storage gives us a versioned record of our broker topology. This is primarily useful for disaster recovery (rebuilding a cluster from scratch), for detecting configuration drift, and for seeding a new cluster during a blue-green upgrade.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Restoring definitions to a running cluster<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">If topology has been accidentally modified or deleted, we can restore from a definitions export without stopping the cluster. The import is additive by default: it creates any missing objects but doesn\u2019t delete objects that exist in the cluster but not in the file.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">curl -s -u admin:yourpassword \\<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">-H &#8220;Content-Type: application\/json&#8221; \\<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">-X POST http:\/\/localhost:15672\/api\/definitions \\<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">-d @rabbitmq-definitions-20260520.json<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Rebuilding a cluster from scratch<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">If we need to replace the entire cluster, the order of operations matters.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">First, if any of the original nodes still exist, we will need to remove them from the cluster view before starting fresh:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">rabbitmqctl forget_cluster_node rabbit@rmq2.internal<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">rabbitmqctl forget_cluster_node rabbit@rmq3.internal<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If we are starting completely fresh:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"># On the first new node<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">rabbitmqctl stop_app<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">rabbitmqctl reset<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">rabbitmqctl start_app<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"># Import definitions<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">curl -s -u admin:yourpassword \\<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">-H &#8220;Content-Type: application\/json&#8221; \\<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">-X POST http:\/\/localhost:15672\/api\/definitions \\<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">-d @rabbitmq-definitions-latest.json<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"># Then join the second and third nodes<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"># (same join_cluster sequence as Part 3)<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">When replacing a single failed node rather than the whole cluster, we will need to provision a new server, copy the Erlang cookie, start RabbitMQ, and join it to the cluster. We will have to remove the old failed node from the cluster\u2019s knowledge with forget_cluster_node before or after joining the replacement:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"># From a healthy node, remove the failed node<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">rabbitmqctl forget_cluster_node rabbit@rmq-failed.internal<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"># On the new replacement node, after copying the Erlang cookie<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">rabbitmqctl stop_app<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">rabbitmqctl join_cluster rabbit@rmq1.internal<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">rabbitmqctl start_app<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Joining the cluster makes the node available but does not automatically restore quorum queue membership. Each queue\u2019s replica set still references the old failed node until we explicitly grow it. We can use rabbitmq-queues grow to add the replacement node to the queues that lost a member:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"># Add the replacement node as a replica to queues that need it<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">rabbitmq-queues grow rabbit@rmq-new.internal all &#8211;vhost-pattern &#8220;.*&#8221; &#8211;queue-pattern &#8220;.*&#8221;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"># Verify replica counts have recovered<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">rabbitmqctl list_queues -p production name leader members online<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Monitor the online column until all queues show three online members.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Rolling Restarts and Upgrades<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Configuration changes, OS patches, and version upgrades all require restarting nodes. And the order in which we do it determines whether the cluster stays available throughout.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Safe restart sequence for a three-node cluster<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">When we need to restart all nodes for a configuration change or OS update, we must always restart nodes hosting the fewest queue leaders first, and the node hosting the most queue leaders last.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">There is no single cluster leader. Each quorum queue independently elects its own leader, and a node may lead anywhere from zero to all of our queues. Restarting a node triggers a leader election only for the queues it leads, which pauses writes on those queues briefly. This is why leaving the most-loaded node for last minimizes total write disruption.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Find the current leader distribution before starting by running:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">rabbitmqctl list_queues -p production name leader<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Then, count how many queues each node leads. Start with the node leading the fewest. Restart one node, wait for it to fully rejoin, then move to the next:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"># On the chosen node<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">systemctl restart rabbitmq-server<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"># From a healthy node, confirm it rejoined<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">rabbitmqctl cluster_status<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Do not restart the next node until the previous one shows up in \u201cRunning Nodes\u201d. Restarting two nodes simultaneously in a three-node cluster will drop the cluster below quorum, making queues unavailable for writes.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Minor version upgrades<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Upgrading from one minor release to another within the same major version (for example, 4.1.x to 4.2.x) is generally supported as a rolling restart, provided the Erlang version stays within the supported range for both the old and new RabbitMQ releases.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">To do that, update the package on one node, restart it, verify it rejoins successfully, and repeat for the remaining nodes. Follow the same sequence as a rolling restart: fewest queue leaders first, most queue leaders last.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"># On each node, one at a time<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">apt-get update<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">apt-get install -y rabbitmq-server<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">systemctl restart rabbitmq-server<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"># Verify<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">rabbitmqctl status<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">rabbitmqctl cluster_status<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Check the RabbitMQ release notes before upgrading to confirm there are no compatibility warnings for our specific release pair.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Major version upgrades<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Rolling upgrades across most major RabbitMQ version pairs aren\u2019t supported, but the 3.13.x to 4.x path is an exception: RabbitMQ officially supports rolling in-place upgrades from 3.13 to 4.x, provided all stable feature flags are enabled on the 3.13 cluster before starting, and you follow the documented node-by-node sequence. For other major version pairs (or when you want a lower-risk migration window), the supported path is a blue-green migration:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Build a new cluster running the target version, using the same configuration as the existing cluster.<\/li>\n\n\n\n<li>Import definitions from the old cluster into the new one.<\/li>\n\n\n\n<li>Move consumers to the new cluster first. The RabbitMQ Federation plugin can bridge the two clusters so consumers on the new cluster pull messages from the old one during the transition.<\/li>\n\n\n\n<li>Once the old cluster\u2019s queues are empty, switch producers to the new cluster.<\/li>\n\n\n\n<li>Decommission the old cluster once all traffic has moved.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">This requires running two clusters simultaneously during the migration window, which has cost and operational overhead.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Erlang version upgrades<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">RabbitMQ runs on the Erlang\/OTP runtime, and each RabbitMQ release requires a specific range of Erlang versions. The <a href=\"https:\/\/www.rabbitmq.com\/docs\/which-erlang\" target=\"_blank\" rel=\"noopener\">RabbitMQ Erlang Version Requirements<\/a> page documents the supported Erlang range for each RabbitMQ release. Check it before upgrading Erlang on any node.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Because the Erlang VM must restart to use a new version, an Erlang upgrade requires the same restart sequence as a RabbitMQ upgrade: nodes hosting fewer queue leaders first, the most-loaded node last. If you are upgrading both Erlang and RabbitMQ at the same time, verify that the new Erlang version is compatible with both your current and target RabbitMQ versions before starting.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">On Ubuntu, Erlang upgrades typically come through apt:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">apt-get update<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">apt-get install -y erlang<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">systemctl restart rabbitmq-server<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">After restarting, confirm the new Erlang version and verify RabbitMQ is still healthy:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">rabbitmqctl status | grep -E &#8220;Erlang|RabbitMQ&#8221;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">rabbitmqctl cluster_status<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Conclusion<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A stable long-term RabbitMQ setup on UpCloud has a few consistent properties: a load balancer that routes clients to healthy nodes and stops sending traffic to alarmed ones, clients that use publisher confirms and handle reconnections correctly, monitoring that surfaces queue depth growth and resource alarms before they become incidents, and a clear runbook for the handful of operational tasks you will repeat: node replacements, rolling restarts, definition exports, and consumer backlog recovery.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Rethink the architecture if the cluster is regularly hitting flow control under normal load and adding capacity doesn\u2019t resolve it, if the queue topology has become difficult to reason about because too many workloads share the same cluster, or if reliability requirements have grown to include multi-region availability.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">A managed service makes sense when the team doesn\u2019t have engineers who can own a messaging layer end to end, when on-call coverage for the broker isn\u2019t feasible, or when the managed service cost is reasonable relative to the engineering time self-hosting requires. Running RabbitMQ on UpCloud gives us control, flexibility, and lower infrastructure costs at scale. It also means the cluster is ours to operate, debug, and maintain. If that tradeoff no longer makes sense, move to a managed service before a preventable incident makes the decision for us.<\/p>\n","protected":false},"author":82,"featured_media":0,"comment_status":"open","ping_status":"closed","template":"","community-category":[223,274],"class_list":["post-9656","tutorial","type-tutorial","status-publish","hentry"],"acf":[],"_links":{"self":[{"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/tutorial\/9656","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/tutorial"}],"about":[{"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/types\/tutorial"}],"author":[{"embeddable":true,"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/users\/82"}],"replies":[{"embeddable":true,"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/comments?post=9656"}],"version-history":[{"count":4,"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/tutorial\/9656\/revisions"}],"predecessor-version":[{"id":9663,"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/tutorial\/9656\/revisions\/9663"}],"wp:attachment":[{"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/media?parent=9656"}],"wp:term":[{"taxonomy":"community-category","embeddable":true,"href":"https:\/\/upcloud.com\/global\/wp-json\/wp\/v2\/community-category?post=9656"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}