Guide · Message Queue

MCP Tools for RabbitMQ — Management API, queue depth, and dead-letter exchange

RabbitMQ exposes two distinct network interfaces: port 5672 for the AMQP protocol used by producers and consumers, and port 15672 for the Management REST API used by operators and tooling. When you build an MCP tool that monitors or manages RabbitMQ — checking queue backlogs, routing dead-lettered messages, inspecting consumer health, or adjusting policies — you use the Management API, not AMQP. Three distinctions separate reliable MCP tooling from fragile tooling: messages_ready vs messages_unacknowledged (these measure different failure modes), consumer count being zero (a queue accumulating depth with zero consumers is the most critical failure mode, distinct from slow consumers), and dead-letter exchange configuration (the DLQ does not exist by default — your MCP tool must verify it is configured before reporting dead-letter depth as a health signal).

TL;DR

Query the RabbitMQ Management API at http://host:15672/api/ with Basic auth (default credentials guest:guest work only from localhost). For queue health, inspect GET /api/queues/{vhost}/{queue} and check both messages_ready (backlog) and messages_unacknowledged (in-flight) separately — a spike in unacknowledged with zero increase in ready signals slow consumers, not a growing backlog. Monitor consumers count going to zero as the highest-priority alert: messages_ready growing with consumers === 0 means nothing is processing the queue. For dead-letter routing, verify the queue has x-dead-letter-exchange set in its arguments, then monitor the DLQ's messages_ready depth separately. Register your RabbitMQ management endpoint health bridge with AliveMCP to detect broker failures independently.

The two ports: AMQP vs Management API

RabbitMQ's architectural split between data-plane and control-plane matters for MCP tooling. The AMQP port (5672, or 5671 for TLS) is for producers sending messages and consumers receiving them — it speaks the AMQP 0-9-1 wire protocol, which requires an AMQP client library. The Management REST API port (15672, or 15671 for TLS) is for observability and administration — it speaks plain HTTP JSON, making it directly callable from any HTTP client without a protocol library.

For MCP tools, the Management API is almost always the right interface. An MCP tool that needs to "check if the order processing queue has a backlog" does not need to produce or consume messages — it needs to read the queue's metric snapshot. The Management API returns this in a single HTTP GET request.

import fetch from 'node-fetch';

class RabbitMQManagementClient {
  constructor(host, port = 15672, username = 'guest', password = 'guest') {
    this.base = `http://${host}:${port}/api`;
    this.auth = Buffer.from(`${username}:${password}`).toString('base64');
  }

  async request(path) {
    const res = await fetch(`${this.base}${path}`, {
      headers: {
        'Authorization': `Basic ${this.auth}`,
        'Accept': 'application/json'
      }
    });
    if (!res.ok) {
      const body = await res.text();
      throw new Error(`RabbitMQ API ${res.status}: ${body}`);
    }
    return res.json();
  }

  async getQueue(vhost, queueName) {
    // vhost '/' must be encoded as '%2F' in the path
    const encodedVhost = encodeURIComponent(vhost);
    return this.request(`/queues/${encodedVhost}/${encodeURIComponent(queueName)}`);
  }

  async listQueues(vhost) {
    const encodedVhost = encodeURIComponent(vhost);
    return this.request(`/queues/${encodedVhost}`);
  }

  async listConsumers(vhost) {
    const encodedVhost = encodeURIComponent(vhost);
    return this.request(`/consumers/${encodedVhost}`);
  }
}

The virtual host (vhost) / is the default namespace in RabbitMQ. It must be URL-encoded as %2F in API paths — /api/queues/%2F/myqueue, not /api/queues///myqueue. Multi-tenant deployments use separate vhosts per tenant; an MCP tool serving multiple tenants must be vhost-aware and validate that the requested vhost exists before querying it.

The Management plugin is an optional RabbitMQ plugin — it must be explicitly enabled. If your deployment does not have it enabled, the port will refuse connections. Check at startup and surface a clear error rather than a cryptic connection refused.

Queue depth: messages_ready vs messages_unacknowledged

The queue depth metric that matters depends on the failure mode you're diagnosing. messages_ready counts messages sitting in the queue waiting to be delivered to a consumer — this is the backlog. messages_unacknowledged counts messages that have been delivered to a consumer but not yet acknowledged — these are in-flight. A third field, messages, is the sum of both.

These two metrics diagnose different problems. A spike in messages_ready with stable messages_unacknowledged indicates that producers are outpacing consumers — either consumers are too slow or there are too few of them. A spike in messages_unacknowledged with stable messages_ready indicates that consumers are receiving messages but not acknowledging them — either they are processing very slowly, they are stuck, or they are failing silently without negative-acknowledging (nacking) the messages.

async function diagnoseQueue(client, vhost, queueName) {
  const q = await client.getQueue(vhost, queueName);

  return {
    name: q.name,
    state: q.state, // 'running', 'idle', 'flow', 'down'
    consumers: q.consumers,
    messages_ready: q.messages_ready,
    messages_unacknowledged: q.messages_unacknowledged,
    messages_total: q.messages,

    // Derived health signals
    consumer_starvation: q.consumers === 0 && q.messages_ready > 0,
    consumer_lag: q.messages_ready,
    inflight_stuck: q.messages_unacknowledged > 0 && q.message_stats?.ack_details?.rate === 0,

    // Throughput rates (messages per second, averaged over recent period)
    publish_rate: q.message_stats?.publish_details?.rate ?? 0,
    deliver_rate: q.message_stats?.deliver_details?.rate ?? 0,
    ack_rate: q.message_stats?.ack_details?.rate ?? 0,

    // Memory pressure
    memory_bytes: q.memory,
    message_bytes_ready: q.message_bytes_ready,
  };
}

The state field is a critical indicator that most monitoring tools miss. A queue in flow state means RabbitMQ is throttling producers to that queue because the consumer or the broker itself cannot keep up — memory high-watermark or disk alarm has triggered flow control. A queue in down state means the queue is on a cluster node that is currently unavailable.

Rate fields (publish_details.rate, deliver_details.rate, ack_details.rate) are rolling averages computed by the Management plugin over configurable intervals (default: samples every 5 seconds, averaged over 60 seconds). They can lag actual activity by up to the sample interval. For real-time rate estimation, take two consecutive snapshots and compute the delta yourself:

async function measureThroughput(client, vhost, queueName, sampleMs = 5000) {
  const before = await client.getQueue(vhost, queueName);
  await new Promise(r => setTimeout(r, sampleMs));
  const after = await client.getQueue(vhost, queueName);

  const deltaTime = sampleMs / 1000;
  // message_stats.deliver_no_ack_details tracks delivered-without-ack (autoAck mode)
  // message_stats.deliver_get_details tracks basic.get (pull-mode delivery)
  return {
    publish_per_sec: (after.message_stats?.publish ?? 0) - (before.message_stats?.publish ?? 0)) / deltaTime,
    ack_per_sec: (after.message_stats?.ack ?? 0) - (before.message_stats?.ack ?? 0)) / deltaTime,
  };
}

Consumer count zero: the highest-priority alert

A queue with consumers === 0 and messages_ready > 0 is the single most actionable RabbitMQ alert. It means messages are accumulating with nothing processing them. This state arises when: all consumer processes have crashed or been stopped; the consumer application failed to reconnect after a broker restart; a code deploy restarted consumers but they failed to re-register due to a configuration error; or a vhost permission change removed the consumer's access.

async function getQueueHealthAlerts(client, vhost) {
  const queues = await client.listQueues(vhost);
  const alerts = [];

  for (const q of queues) {
    if (q.consumers === 0 && q.messages_ready > 0) {
      alerts.push({
        severity: 'critical',
        queue: q.name,
        reason: 'consumer_starvation',
        messages_ready: q.messages_ready,
        message: `Queue ${q.name} has ${q.messages_ready} ready messages and no consumers`
      });
    } else if (q.messages_ready > 10000) {
      alerts.push({
        severity: 'warning',
        queue: q.name,
        reason: 'backlog_high',
        messages_ready: q.messages_ready,
        consumers: q.consumers,
        message: `Queue ${q.name} backlog is ${q.messages_ready} (${q.consumers} consumers)`
      });
    } else if (q.messages_unacknowledged > q.consumers * 100) {
      // More than 100 unacked per consumer suggests processing is stuck
      alerts.push({
        severity: 'warning',
        queue: q.name,
        reason: 'inflight_high',
        messages_unacknowledged: q.messages_unacknowledged,
        consumers: q.consumers,
        message: `Queue ${q.name} has ${q.messages_unacknowledged} unacked across ${q.consumers} consumers`
      });
    }
  }

  return alerts;
}

The x-max-length and x-overflow arguments control what happens when a queue reaches its maximum length. With x-overflow: "drop-head" (the default), the oldest messages are dropped silently when the queue is full. With x-overflow: "reject-publish", publishers receive a negative confirm and must handle the rejection. With x-overflow: "reject-publish-dlx", rejected messages are dead-lettered. An MCP tool checking queue length limits should also check whether overflow is silently dropping messages, since this is invisible to consumer-side metrics.

Dead-letter exchange: configuration and monitoring

Dead-letter exchange (DLX) is RabbitMQ's mechanism for handling messages that cannot be processed. A message is dead-lettered when: it is nacked (basic.nack or basic.reject) without requeue; its per-message or queue-level TTL expires; or the queue is at its maximum length with x-overflow: "reject-publish-dlx". Dead-lettered messages are republished to the configured x-dead-letter-exchange, which then routes them based on normal exchange routing rules.

The DLQ does not exist by default. An MCP tool that reports "DLQ depth" without first verifying the DLX is configured on the queue is reporting a meaningless zero. Always inspect the queue's arguments field to confirm the DLX is set:

async function getDLQStatus(client, vhost, queueName) {
  const q = await client.getQueue(vhost, queueName);
  const dlxName = q.arguments?.['x-dead-letter-exchange'];
  const dlRoutingKey = q.arguments?.['x-dead-letter-routing-key'];

  if (!dlxName) {
    return {
      dlx_configured: false,
      message: `Queue ${queueName} has no x-dead-letter-exchange configured`
    };
  }

  // The DLQ is typically a queue bound to the DLX.
  // Naming convention varies by team but commonly: -dlq or .dlq
  // Use the exchange bindings API to discover which queues receive dead-lettered messages:
  const encodedVhost = encodeURIComponent(vhost);
  const encodedDlx = encodeURIComponent(dlxName);
  const bindings = await client.request(`/bindings/${encodedVhost}/e/${encodedDlx}/q`);

  const dlqQueues = await Promise.all(
    bindings.map(b => client.getQueue(vhost, b.destination))
  );

  return {
    dlx_configured: true,
    dlx_name: dlxName,
    dl_routing_key: dlRoutingKey ?? '(original routing key preserved)',
    dlq_queues: dlqQueues.map(q => ({
      name: q.name,
      messages_ready: q.messages_ready,
      consumers: q.consumers,
      // Zero consumers on DLQ means dead-lettered messages are accumulating unreviewed
      unmonitored: q.consumers === 0 && q.messages_ready > 0,
    }))
  };
}

An important subtlety: dead-lettered messages carry additional headers that RabbitMQ appends — the x-death header array records the death history (queue name, exchange, routing key, reason, count, time). Each time a message is re-dead-lettered, a new entry is appended (or the count on an existing entry is incremented if the queue+reason combination matches). An MCP tool reading from a DLQ should inspect x-death[0].count to understand how many times a message has been requeued and failed — this is the retry count that indicates persistent processing failures vs transient ones.

Prefetch (QoS) and flow control

Channel-level prefetch (basic.qos, set via the AMQP protocol's prefetch_count parameter) controls how many unacknowledged messages a consumer can hold at once. This is not visible in the Management API's queue metrics directly, but it drives the relationship between messages_unacknowledged and consumer count. If a queue has 5 consumers each with prefetch=10, the maximum messages_unacknowledged is 50. If unacknowledged exceeds consumers × prefetch, that means multiple channels per consumer connection.

The consumer list API reveals per-consumer prefetch settings:

async function getConsumerDetails(client, vhost, queueName) {
  const consumers = await client.listConsumers(vhost);
  const queueConsumers = consumers.filter(c => c.queue.name === queueName);

  return queueConsumers.map(c => ({
    consumer_tag: c.consumer_tag,
    channel: c.channel_details.name,
    connection: c.channel_details.connection_name,
    prefetch_count: c.prefetch_count, // 0 means unlimited (dangerous)
    ack_required: c.ack_required,     // false = auto-ack (messages lost on crash)
    exclusive: c.exclusive,
    active: c.active,                 // false if consumer is suspended (flow-controlled)
  }));
}

Prefetch of 0 (unlimited) is dangerous in production: a single consumer will pull every ready message from the queue into its own in-flight buffer, making the queue appear empty to other consumers while the messages pile up in the consumer process's memory. This is one of the most common causes of "queue looks healthy but orders are delayed" — all messages are unacknowledged in one consumer's buffer. An MCP tool should flag any consumer with prefetch_count === 0.

Virtual host isolation and policy-based TTL

Virtual hosts partition a RabbitMQ broker into isolated namespaces: separate exchanges, queues, bindings, user permissions, and message storage. Queues with the same name in different vhosts are completely independent. An MCP tool that operates on queues must take vhost as a parameter and validate it before querying:

async function validateVhost(client, vhost) {
  try {
    const vhosts = await client.request('/vhosts');
    const exists = vhosts.some(v => v.name === vhost);
    if (!exists) {
      throw new Error(`Virtual host '${vhost}' does not exist. Available: ${vhosts.map(v => v.name).join(', ')}`);
    }
    return true;
  } catch (err) {
    if (err.message.includes('401')) throw new Error('Invalid credentials for RabbitMQ Management API');
    throw err;
  }
}

Policies apply configuration to matching exchanges or queues within a vhost without requiring queue deletion and recreation. The most common policy parameters for MCP tooling awareness:

async function getAppliedPolicies(client, vhost, queueName) {
  const q = await client.getQueue(vhost, queueName);
  // 'effective_policy_definition' merges all applicable policies
  return {
    policy_name: q.policy,
    effective_definition: q.effective_policy_definition ?? {},
    // Per-queue arguments override policies for the same key
    queue_arguments: q.arguments ?? {},
    message_ttl_ms: q.effective_policy_definition?.['message-ttl']
      ?? q.arguments?.['x-message-ttl'],
    max_length: q.effective_policy_definition?.['max-length']
      ?? q.arguments?.['x-max-length'],
  };
}

Connecting RabbitMQ health to AliveMCP

RabbitMQ's Management API is an HTTP endpoint, but it requires authentication and does not expose a single unauthenticated health route by default. To monitor RabbitMQ availability with AliveMCP, expose a thin health bridge from your MCP server or a sidecar service:

import express from 'express';
const app = express();
const rabbitClient = new RabbitMQManagementClient(process.env.RABBITMQ_HOST);

app.get('/health/rabbitmq', async (req, res) => {
  try {
    // /api/health/checks/alarms is RabbitMQ's own health endpoint (3.8+)
    const health = await rabbitClient.request('/health/checks/alarms');
    const alerts = await getQueueHealthAlerts(rabbitClient, '/');
    const criticals = alerts.filter(a => a.severity === 'critical');

    if (criticals.length > 0) {
      return res.status(503).json({ status: 'unhealthy', alerts: criticals });
    }
    res.json({ status: 'healthy', warnings: alerts.filter(a => a.severity === 'warning') });
  } catch (err) {
    res.status(503).json({ status: 'unreachable', error: err.message });
  }
});

app.listen(8080);

Register the /health/rabbitmq endpoint with AliveMCP. This gives you external monitoring of RabbitMQ availability that survives MCP server restarts, and triggers alerts on queue consumer starvation before users notice message processing delays.

Frequently asked questions

What is the difference between messages_ready and messages_unacknowledged in RabbitMQ?

messages_ready counts messages waiting in the queue to be delivered to a consumer — this is the backlog that grows when producers outpace consumers. messages_unacknowledged counts messages that have been delivered to a consumer channel but have not yet been acknowledged (basic.ack) or rejected (basic.nack/basic.reject) — these are in-flight. The sum of both is the total messages field. A sudden spike in messages_unacknowledged without a corresponding increase in messages_ready indicates that consumers received messages but are processing them abnormally slowly, or are stuck waiting on an external dependency. A spike in messages_ready with stable messages_unacknowledged indicates that the producer rate exceeds the consumer processing rate. An MCP tool that only checks the total messages field misses this distinction.

Why must I encode the default vhost "/" as "%2F" in Management API URLs?

The vhost name is embedded in the URL path. The default vhost is named / (a single forward slash), which is also the URL path separator character. Without encoding, /api/queues///myqueue would be interpreted by HTTP routing as a path with three segments rather than a vhost named / and a queue named myqueue. URL-encoding / as %2F produces /api/queues/%2F/myqueue, which HTTP routers preserve literally. Use encodeURIComponent(vhost) for the vhost segment and encodeURIComponent(queueName) for the queue name — queue names can contain characters like : and # that are meaningful in URL syntax.

How do I find which queue is receiving dead-lettered messages from a source queue?

Dead-lettered messages are published to the exchange named in the queue's x-dead-letter-exchange argument, using the routing key from x-dead-letter-routing-key (or the original routing key if not set). To find the DLQ, query the bindings for the dead-letter exchange: GET /api/bindings/{vhost}/e/{dlx-name}/q returns all queue bindings on that exchange. Each binding's destination field is a queue name that receives dead-lettered messages with a matching routing key. If no binding matches the dead-letter routing key, dead-lettered messages are silently discarded — a common misconfiguration where teams set up the DLX but forget to bind a queue to it.

How does prefetch_count affect queue depth metrics?

Each consumer channel holds up to prefetch_count messages in its client-side buffer. These messages have already been removed from the queue's messages_ready count and moved to messages_unacknowledged. With prefetch_count=0 (unlimited), a consumer will drain the entire queue's ready messages into its own buffer instantly, making the queue appear empty while all messages sit unacknowledged in one consumer's memory. If that consumer crashes, all buffered messages become redelivered (if they have been delivered once, the redelivered flag is set on redelivery). This means a queue with depth=0 and a large messages_unacknowledged count is not actually empty — it just has all its messages in consumer buffers. For MCP tools, always report both messages_ready and messages_unacknowledged rather than just the total.

How do I monitor RabbitMQ cluster node health via the Management API?

The Management API exposes cluster node health at GET /api/nodes (all nodes) and GET /api/nodes/{node-name} (specific node). Key fields: running (boolean — false means the node is down), disk_free_alarm and mem_alarm (boolean — true means the node is in flow control because of disk or memory pressure), disk_free (bytes — compare against disk_free_limit to see how close to the alarm threshold), mem_used vs mem_limit. A node with both running: true and disk_free_alarm: true is up but throttling all publish operations — this is a common cause of intermittent message delivery delays. RabbitMQ 3.8+ also provides GET /api/health/checks/alarms which returns a structured health check result aggregated across all nodes.

Further reading

Know when your MCP server is down — before users do

AliveMCP probes your server's MCP endpoint every minute, detects protocol errors and transport failures, and pages you before users notice.

Start monitoring free