Chapters

15 AMQP Operations: Deployment and Broker Health

amqp
impl
production
reliability
rabbitmq

Start with the story: Production AMQP is the discipline of keeping a busy message factory honest. Exchanges accept load, queues store backlog, consumers drain work, and operators watch the evidence before delay becomes data loss.

15.1 Start With the Decision

An AMQP broker can accept traffic while queues grow toward failure. Operators need limits, health signals, and alerts before service drops.

15.2 Route Overview

This is part 1 of 2. Continue with AMQP Operations: Capacity Tools and Diagnostics.

15.3 Part Objectives

  • Plan AMQP deployment, scaling, and recovery controls.
  • Monitor queue, connection, and broker health indicators.
In 60 Seconds

15.3.1 Make One Message Survive a Bad Night

A freezer sends a warning while the duty worker is asleep. The warning enters a message service, waits for a worker program, and should end as a checked repair. A green send light proves very little. The operations owner needs to know whether the warning was accepted, kept, delivered, handled once, and linked to the final action.

Draw that one path before tuning the system. Mark where the sender hands over responsibility. Mark where work waits and where a worker confirms completion. Give failed work a visible holding place. Set an owner and age limit for every waiting item. These boundaries turn a vague promise of reliable delivery into checks that another person can repeat.

Then cause the failures expected in real service. Restart the message service after acceptance. Pause the worker. Send the same item twice. Send one item the worker cannot read. Fill the waiting area. Check that useful work survives, duplicates do not repeat the physical action, and failed items remain visible with a reason.

This path does not prove that every business action finished merely because transport succeeded. It proves only the named delivery boundaries. The deeper sections connect those boundaries to durable settings, sender and worker confirmations, failure routing, health measures, and capacity choices in several client languages.

Production AMQP deployments require durable exchanges and queues that survive broker restarts, publisher confirms and manual consumer acknowledgments for end-to-end reliability, dead-letter queues for failed message investigation, and monitoring of queue depth, throughput, and consumer lag. This chapter provides production-ready configurations for Python (Pika), Java, and Node.js with patterns for integrating AMQP into edge computing and cloud IoT architectures.

Chapter Roadmap
  • In 60 Seconds
  • Key Concepts
  • Prerequisites
  • For Beginners: Production vs Development
  • Going Live!
  • AMQP Implementation Patterns
  • Production Configuration Checklist
  • Checkpoint: production defaults
  • Try It: AMQP Message Exchange Simulator on ESP32
  • Try It: AMQP Exchange Type Routing Explorer
  • Python Implementation with Pika
  • Java Implementation
  • Node.js Implementation
  • Checkpoint: client contracts
  • Dead Letter Queue Configuration
  • Try It: Dead Letter Queue Simulator
  • Checkpoint: failure routing
  • Monitoring and Alerting
  • Try It: AMQP Health Monitor Dashboard
  • Checkpoint: broker health

15.4 Learning Objectives

By the end of this chapter, you will be able to:

  • Configure Production-Ready AMQP Systems: Set up durable exchanges, queues, and bindings with appropriate reliability settings that survive broker restarts
  • Implement Client Libraries: Construct AMQP producers and consumers using Python (Pika), Java, and Node.js with publisher confirms and manual acknowledgment
  • Apply Reliability Patterns: Configure publisher confirms, consumer acknowledgments, and dead letter queues to achieve end-to-end message delivery guarantees
  • Diagnose AMQP Health: Assess queue depth, message throughput, and consumer lag metrics to identify bottlenecks and plan capacity
  • Select Exchange Topologies: Compare direct, fanout, topic, and headers exchanges and justify the choice based on routing requirements and IoT use cases
  • Calculate Broker Sizing: Determine memory, queue depth limits, and consumer counts required for a given IoT message throughput target
  • Integrate with IoT Architectures: Demonstrate how AMQP connects to edge computing, cloud platforms, and data analytics pipelines in production deployments

Key Concepts

First: AMQP: Advanced Message Queuing Protocol — open standard for enterprise message routing with delivery guarantees

Next: Exchange Types: Direct (exact key), Topic (wildcard), Fanout (broadcast), Headers (metadata) — four routing strategies

Then: Queue: Message buffer between exchange and consumer — durable queues survive broker restarts

After that: Binding: Connection between exchange and queue specifying routing key pattern for message matching

Also inspect: Delivery Guarantee: At-most-once (auto-ack), at-least-once (manual-ack + persistence), exactly-once (transactions)

Finally: Publisher Confirms: Asynchronous broker acknowledgment to producers confirming message persistence in the queue

Finally: Dead Letter Exchange: Secondary exchange receiving rejected, expired, or overflowed messages for error handling

15.5 Prerequisites

Before diving into this chapter, you should be familiar with:

Production AMQP systems differ from development setups in several key ways:

First: Durability: All queues and exchanges must survive broker restarts

Next: Monitoring: Queue depth, message rates, and consumer health must be tracked

Then: Error Handling: Dead letter queues capture undeliverable messages for investigation

After that: Scaling: Multiple consumers and high availability configurations

This chapter provides production-ready configurations you can adapt to your specific requirements.

“Our smart greenhouse project works great in testing,” said Temperature Terry. “But what happens when we have 500 sensors instead of 5?”

the microcontroller pulled out a checklist. “Production is a whole different game, Sammy! First, you need connection pooling — instead of each sensor opening its own connection, groups of sensors share connections. It’s like carpooling instead of everyone driving solo.”

“What about when things go wrong?” asked the LED. “That’s where monitoring comes in,” Max replied. “You watch your queue depths like a traffic report. If messages are piling up in a queue, it means consumers can’t keep up. You either add more consumers or figure out why they’re slow.”

the battery raised a concern: “And what about my power budget?” Max smiled. “Use heartbeats wisely — they keep connections alive but cost energy. Set them to 60 seconds instead of 10. And enable prefetch limits so consumers don’t grab more messages than they can handle. In production, efficiency isn’t optional — it’s survival!”

15.6 AMQP Implementation Patterns

Key implementation patterns for production AMQP systems:

Inspect Figure 15.1 to place implementation choices along one message path before tuning individual client settings.

Production AMQP design map separating the routing contract, durable publish boundary with confirms, bounded consumption with acknowledgments and idempotency, and operational recovery with retry and dead-letter routing.
Figure 15.1: AMQP implementation patterns for routing, delivery, reliability, and performance

Trace Figure 15.1 from publish through routing and queueing to consumer completion, then inspect the reliability and performance controls attached to each boundary. Confirms, persistence, acknowledgments, dead-lettering, and prefetch solve different failures. The map therefore becomes a checklist for the production configuration that follows.

15.7 Production Configuration Checklist

Implementation checklist for production AMQP systems:

  • Exchange durable=True Survives broker restart
  • Queue durable=True, auto_delete=False Persists messages when consumer offline
  • Messages delivery_mode=2 Written to disk before ACK
  • Publisher confirm_select() Receive broker acknowledgments
  • Consumer auto_ack=False, prefetch_count=N Manual ACK with batching
  • Dead Letter x-dead-letter-exchange Handle undeliverable messages
Broker BexCheckpoint: production defaults

You now know:

  • Durable exchanges and durable queues protect definitions across broker restarts; persistent messages require delivery_mode=2.
  • Publisher confirms and mandatory=True prove the broker accepted a routable message instead of silently dropping it.
  • Manual acknowledgment pairs auto_ack=False with bounded prefetch so retries stay visible.

Objective: Simulate AMQP exchange types (direct, fanout, topic) and message routing on ESP32, demonstrating how producers publish to exchanges, routing keys determine queue delivery, and consumers acknowledge messages — the same patterns used in production RabbitMQ deployments.

Paste this code into the Wokwi editor:

#include <WiFi.h>

// Simulated AMQP components
struct Message {
  const char* routingKey;
  const char* body;
  int deliveryMode;  // 1=transient, 2=persistent
  bool acked;
};

struct Queue {
  const char* name;
  const char* bindingKey;
  bool durable;
  int messageCount;
  int ackedCount;
  int nackedCount;
};

void setup() {
  Serial.begin(115200);
  delay(1000);

  Serial.println("=== AMQP Exchange Routing Simulator ===\n");

  // === 1. Direct Exchange ===
  Serial.println("--- Direct Exchange: 'sensor-alerts' ---");
  Serial.println("Routing: exact match between routing key and binding key\n");

  Queue directQueues[] = {
    {"temp-critical", "alert.temperature.critical", true, 0, 0, 0},
    {"humidity-warn", "alert.humidity.warning", true, 0, 0, 0},
    {"all-alerts",    "alert.temperature.critical", true, 0, 0, 0}
  };

  Message directMsgs[] = {
    {"alert.temperature.critical", "{\"temp\":85,\"unit\":\"C\"}", 2, false},
    {"alert.humidity.warning", "{\"humidity\":92,\"unit\":\"%\"}", 2, false},
    {"alert.pressure.info", "{\"pressure\":1013,\"unit\":\"hPa\"}", 1, false}
  };

  for (int m = 0; m < 3; m++) {
    Serial.printf("PUBLISH key='%s' | persistent=%s\n",
                  directMsgs[m].routingKey,
                  directMsgs[m].deliveryMode == 2 ? "yes" : "no");
    bool routed = false;
    for (int q = 0; q < 3; q++) {
      if (strcmp(directMsgs[m].routingKey, directQueues[q].bindingKey) == 0) {
        directQueues[q].messageCount++;
        routed = true;
        Serial.printf("  -> Delivered to '%s' (depth: %d)\n",
                      directQueues[q].name, directQueues[q].messageCount);
      }
    }
    if (!routed) {
      Serial.println("  -> UNROUTABLE (no matching binding) - message DROPPED!");
      Serial.println("     FIX: Configure alternate-exchange or mandatory flag");
    }
  }

  // === 2. Fanout Exchange ===
  Serial.println("\n--- Fanout Exchange: 'system-broadcast' ---");
  Serial.println("Routing: all bound queues receive every message\n");

  const char* fanoutQueues[] = {"logging", "analytics", "backup"};
  Serial.println("PUBLISH key='ignored' body='{\"event\":\"system.restart\"}'");
  for (int q = 0; q < 3; q++) {
    Serial.printf("  -> Delivered to '%s' (fanout ignores routing key)\n",
                  fanoutQueues[q]);
  }

  // === 3. Topic Exchange ===
  Serial.println("\n--- Topic Exchange: 'iot-telemetry' ---");
  Serial.println("Routing: pattern matching (* = one word, # = zero or more)\n");

  struct TopicBinding {
    const char* queue;
    const char* pattern;
  };

  TopicBinding topicBindings[] = {
    {"floor1-all",      "factory.floor1.*"},
    {"all-temperature", "factory.*.temperature"},
    {"everything",      "factory.#"},
    {"emergency",       "factory.*.emergency"}
  };

  const char* topicMsgs[] = {
    "factory.floor1.temperature",
    "factory.floor1.humidity",
    "factory.floor2.temperature",
    "factory.floor1.emergency",
    "factory.floor2.line3.vibration"
  };

  for (int m = 0; m < 5; m++) {
    Serial.printf("PUBLISH key='%s'\n", topicMsgs[m]);
    for (int b = 0; b < 4; b++) {
      bool match = false;
      // Simple pattern matching simulation
      String key = topicMsgs[m];
      String pattern = topicBindings[b].pattern;

      if (pattern.endsWith("#")) {
        String prefix = pattern.substring(0, pattern.length() - 1);
        match = key.startsWith(prefix);
      } else if (pattern.indexOf('*') >= 0) {
        // Count dots to check word count matches
        int keyDots = 0, patDots = 0;
        for (int i = 0; i < key.length(); i++) if (key[i] == '.') keyDots++;
        for (int i = 0; i < pattern.length(); i++) if (pattern[i] == '.') patDots++;
        if (keyDots == patDots) {
          // Check non-wildcard segments
          match = true;
          int ki = 0, pi = 0;
          while (ki < key.length() && pi < pattern.length()) {
            if (pattern[pi] == '*') {
              while (ki < key.length() && key[ki] != '.') ki++;
              while (pi < pattern.length() && pattern[pi] != '.') pi++;
            } else if (key[ki] == pattern[pi]) {
              ki++; pi++;
            } else {
              match = false; break;
            }
          }
        }
      } else {
        match = (key == pattern);
      }

      if (match) {
        Serial.printf("  -> Matched '%s' (pattern: %s)\n",
                      topicBindings[b].queue, topicBindings[b].pattern);
      }
    }
  }

  // === 4. Consumer Acknowledgment Demo ===
  Serial.println("\n--- Consumer Acknowledgment Patterns ---\n");

  Serial.println("Auto-ack (DANGEROUS for critical data):");
  Serial.println("  Consumer receives msg -> ACK sent immediately");
  Serial.println("  Consumer CRASHES during processing -> MESSAGE LOST!\n");

  Serial.println("Manual-ack (SAFE for production):");
  Serial.println("  Consumer receives msg -> Processes msg -> Sends ACK");
  Serial.println("  Consumer CRASHES during processing -> Msg REDELIVERED\n");

  Serial.println("Prefetch=10 with manual ack:");
  for (int i = 1; i <= 10; i++) {
    Serial.printf("  [%02d] Received -> Processing... -> ACK\n", i);
  }
  Serial.println("  Broker delivers next batch of 10\n");

  Serial.println("=== AMQP Exchange Routing Demo Complete ===");
}

void loop() {
  delay(10000);
}

What to Observe:

First: Direct exchange routes only to queues with an exact routing key match — the “pressure.info” message is dropped because no queue binds that key (a common production data loss scenario)

Next: Topic exchange patterns: factory.floor1.* matches exactly 3-word keys starting with factory.floor1, while factory.# matches ALL messages (including the 5-word factory.floor2.line3.vibration)

Then: Fanout exchange ignores routing keys entirely — every bound queue gets every message, perfect for logging and analytics

After that: Auto-ack vs manual-ack is the #1 cause of production data loss — auto-ack acknowledges before processing, so a crash loses the message permanently

Try It: AMQP Exchange Type Routing Explorer

Select an exchange type and publish messages with different routing keys to see which queues receive each message. Observe how direct, fanout, topic, and headers exchanges differ in routing behavior.

The simulator showed why routing and acknowledgment choices are operational controls. The next sections express them in three client libraries.

15.8 Python Implementation with Pika

15.8.1 Publisher with Confirms

import pika
import json
from typing import Dict, Any

class AMQPPublisher:
    """Production-ready AMQP publisher with reliability features."""

    def __init__(self, host: str, exchange: str):
        self.connection = pika.BlockingConnection(
            pika.ConnectionParameters(
                host=host,
                heartbeat=600,  # Detect dead connections
                blocked_connection_timeout=300
            )
        )
        self.channel = self.connection.channel()
        self.exchange = exchange

        # Enable publisher confirms
        self.channel.confirm_delivery()

        # Declare durable exchange
        self.channel.exchange_declare(
            exchange=exchange,
            exchange_type='topic',
            durable=True
        )

    def publish(self, routing_key: str, message: Dict[str, Any],
                message_id: str = None) -> bool:
        """
        Publish message with persistence and confirmation.

        Args:
            routing_key: Topic routing key (e.g., 'sensor.temperature.line1')
            message: Dictionary to serialize as JSON
            message_id: Unique ID for idempotency (optional)

        Returns:
            True if broker acknowledged, False otherwise
        """
        properties = pika.BasicProperties(
            delivery_mode=2,  # Persistent
            content_type='application/json',
            message_id=message_id
        )

        try:
            self.channel.basic_publish(
                exchange=self.exchange,
                routing_key=routing_key,
                body=json.dumps(message),
                properties=properties,
                mandatory=True  # Return if unroutable
            )
            return True
        except pika.exceptions.UnroutableError:
            print(f"Message unroutable: {routing_key}")
            return False

    def close(self):
        """Clean shutdown."""
        self.connection.close()


# Usage example
publisher = AMQPPublisher('localhost', 'sensor-data')
publisher.publish(
    routing_key='sensor.temperature.line1.machine3',
    message={'temp': 75.3, 'timestamp': 1698765432},
    message_id='msg-001'
)

15.8.2 Consumer with Manual Acknowledgment

Read the Python consumer from queue declaration through bindings and prefetch before tracing the callback outcomes. The success path acknowledges only after processing; retryable failure requeues; terminal failure rejects without requeue so the configured dead-letter exchange preserves evidence.

import pika
import json
from typing import Callable

class AMQPConsumer:
    """Production-ready AMQP consumer with reliability features."""

    def __init__(self, host: str, queue: str, exchange: str,
                 binding_patterns: list, prefetch_count: int = 10):
        self.connection = pika.BlockingConnection(
            pika.ConnectionParameters(host=host)
        )
        self.channel = self.connection.channel()
        self.queue = queue

        # Declare durable queue with dead letter exchange
        self.channel.queue_declare(
            queue=queue,
            durable=True,
            arguments={
                'x-dead-letter-exchange': 'dlx',
                'x-dead-letter-routing-key': f'{queue}.failed'
            }
        )

        # Bind to exchange with patterns
        for pattern in binding_patterns:
            self.channel.queue_bind(
                exchange=exchange,
                queue=queue,
                routing_key=pattern
            )

        # Set prefetch for batching
        self.channel.basic_qos(prefetch_count=prefetch_count)

    def consume(self, callback: Callable):
        """
        Start consuming messages with manual acknowledgment.

        Args:
            callback: Function(body: dict) -> bool
                     Returns True if processed successfully
        """
        def on_message(channel, method, properties, body):
            try:
                message = json.loads(body)
                success = callback(message)

                if success:
                    channel.basic_ack(delivery_tag=method.delivery_tag)
                else:
                    # Requeue for retry
                    channel.basic_nack(
                        delivery_tag=method.delivery_tag,
                        requeue=True
                    )
            except Exception as e:
                print(f"Processing error: {e}")
                # Send to dead letter queue
                channel.basic_nack(
                    delivery_tag=method.delivery_tag,
                    requeue=False
                )

        self.channel.basic_consume(
            queue=self.queue,
            on_message_callback=on_message,
            auto_ack=False  # Manual acknowledgment
        )

        print(f"Consuming from {self.queue}...")
        self.channel.start_consuming()


# Usage example
def process_sensor_reading(message: dict) -> bool:
    """Process a sensor reading. Returns True if successful."""
    print(f"Received: {message}")
    # Your processing logic here
    return True

consumer = AMQPConsumer(
    host='localhost',
    queue='analytics',
    exchange='sensor-data',
    binding_patterns=['sensor.#'],
    prefetch_count=100  # Batch processing
)
consumer.consume(process_sensor_reading)

15.9 Java Implementation

15.9.1 Publisher with RabbitMQ Java Client

import com.rabbitmq.client.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class AMQPPublisher implements AutoCloseable {
    private final Connection connection;
    private final Channel channel;
    private final String exchange;
    private final ObjectMapper mapper = new ObjectMapper();

    public AMQPPublisher(String host, String exchange)
            throws IOException, TimeoutException {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost(host);
        factory.setRequestedHeartbeat(60);

        this.connection = factory.newConnection();
        this.channel = connection.createChannel();
        this.exchange = exchange;

        // Enable publisher confirms
        channel.confirmSelect();

        // Declare durable topic exchange
        channel.exchangeDeclare(exchange, "topic", true);
    }

    public boolean publish(String routingKey, Object message,
            String messageId) throws Exception {
        AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
            .deliveryMode(2)  // Persistent
            .contentType("application/json")
            .messageId(messageId)
            .build();

        byte[] body = mapper.writeValueAsBytes(message);

        channel.basicPublish(exchange, routingKey, true, properties, body);

        // Wait for confirm (blocking)
        return channel.waitForConfirms(5000);
    }

    @Override
    public void close() throws Exception {
        channel.close();
        connection.close();
    }
}

15.9.2 Consumer with Manual Acknowledgment

Read the Java consumer in the same order as the Python contract: durable queue and dead-letter arguments first, binding patterns second, bounded prefetch third, and explicit basicAck or basicNack only after the callback reports its outcome.

import com.rabbitmq.client.*;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.util.Map;
import java.util.function.Function;

public class AMQPConsumer {
    private final Channel channel;
    private final String queue;
    private final ObjectMapper mapper = new ObjectMapper();

    public AMQPConsumer(String host, String queue, String exchange,
            String[] bindingPatterns, int prefetchCount) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setHost(host);

        Connection connection = factory.newConnection();
        this.channel = connection.createChannel();
        this.queue = queue;

        // Declare queue with DLX
        Map<String, Object> args = Map.of(
            "x-dead-letter-exchange", "dlx",
            "x-dead-letter-routing-key", queue + ".failed"
        );
        channel.queueDeclare(queue, true, false, false, args);

        // Bind patterns
        for (String pattern : bindingPatterns) {
            channel.queueBind(queue, exchange, pattern);
        }

        // Set prefetch
        channel.basicQos(prefetchCount);
    }

    public void consume(Function<Map<String, Object>, Boolean> callback)
            throws IOException {
        DeliverCallback deliverCallback = (tag, delivery) -> {
            try {
                Map<String, Object> message = mapper.readValue(
                    delivery.getBody(), Map.class);

                boolean success = callback.apply(message);

                if (success) {
                    channel.basicAck(delivery.getEnvelope()
                        .getDeliveryTag(), false);
                } else {
                    channel.basicNack(delivery.getEnvelope()
                        .getDeliveryTag(), false, true);
                }
            } catch (Exception e) {
                channel.basicNack(delivery.getEnvelope()
                    .getDeliveryTag(), false, false);
            }
        };

        channel.basicConsume(queue, false, deliverCallback, tag -> {});
    }
}

15.10 Node.js Implementation

15.10.1 Publisher with amqplib

const amqp = require('amqplib');

class AMQPPublisher {
    constructor() {
        this.connection = null;
        this.channel = null;
    }

    async connect(host, exchange) {
        this.connection = await amqp.connect(`amqp://${host}`);
        this.channel = await this.connection.createConfirmChannel();
        this.exchange = exchange;

        // Declare durable topic exchange
        await this.channel.assertExchange(exchange, 'topic', {
            durable: true
        });
    }

    async publish(routingKey, message, messageId = null) {
        const options = {
            persistent: true,  // delivery_mode = 2
            contentType: 'application/json',
            messageId: messageId
        };

        return new Promise((resolve, reject) => {
            this.channel.publish(
                this.exchange,
                routingKey,
                Buffer.from(JSON.stringify(message)),
                options,
                (err) => {
                    if (err) reject(err);
                    else resolve(true);
                }
            );
        });
    }

    async close() {
        await this.channel.close();
        await this.connection.close();
    }
}

// Usage
(async () => {
    const publisher = new AMQPPublisher();
    await publisher.connect('localhost', 'sensor-data');

    await publisher.publish(
        'sensor.temperature.line1.machine3',
        { temp: 75.3, timestamp: Date.now() },
        'msg-001'
    );

    await publisher.close();
})();

15.10.2 Consumer with Manual Acknowledgment

Read the Node.js consumer from connection and queue setup into pattern bindings, prefetch, and the asynchronous callback. The final ACK, requeueing NACK, or dead-lettering NACK must describe processing outcome; receiving the message alone is not completion evidence.

const amqp = require('amqplib');

class AMQPConsumer {
    async connect(host, queue, exchange, bindingPatterns, prefetchCount = 10) {
        this.connection = await amqp.connect(`amqp://${host}`);
        this.channel = await this.connection.createChannel();
        this.queue = queue;

        // Declare queue with DLX
        await this.channel.assertQueue(queue, {
            durable: true,
            arguments: {
                'x-dead-letter-exchange': 'dlx',
                'x-dead-letter-routing-key': `${queue}.failed`
            }
        });

        // Bind patterns
        for (const pattern of bindingPatterns) {
            await this.channel.bindQueue(queue, exchange, pattern);
        }

        // Set prefetch
        await this.channel.prefetch(prefetchCount);
    }

    async consume(callback) {
        console.log(`Consuming from ${this.queue}...`);

        this.channel.consume(this.queue, async (msg) => {
            if (msg === null) return;

            try {
                const message = JSON.parse(msg.content.toString());
                const success = await callback(message);

                if (success) {
                    this.channel.ack(msg);
                } else {
                    this.channel.nack(msg, false, true);  // Requeue
                }
            } catch (error) {
                console.error('Processing error:', error);
                this.channel.nack(msg, false, false);  // Dead letter
            }
        }, { noAck: false });
    }
}

// Usage
(async () => {
    const consumer = new AMQPConsumer();
    await consumer.connect(
        'localhost',
        'analytics',
        'sensor-data',
        ['sensor.#'],
        100
    );

    await consumer.consume((message) => {
        console.log('Received:', message);
        return true;  // Processing successful
    });
})();

Broker BexCheckpoint: client contracts

You now know:

  • Python uses confirm_delivery(), Java uses confirmSelect(), and Node.js uses createConfirmChannel() for publisher confirms.
  • Each consumer declares a durable queue with x-dead-letter-exchange and limits in-flight work with prefetch control.
  • The examples keep sensor.#, analytics, and sensor-data visible so routing, queue ownership, and exchange ownership can be reviewed together.

The client code is reliable only if failed messages have a bounded destination. The dead-letter section makes that destination inspectable.

15.11 Dead Letter Queue Configuration

Dead letter queues capture messages that cannot be processed for later investigation:

# Declare dead letter exchange and queue
channel.exchange_declare(exchange='dlx', exchange_type='direct', durable=True)
channel.queue_declare(queue='dead-letters', durable=True)
channel.queue_bind(exchange='dlx', queue='dead-letters', routing_key='#')

# Main queue with DLX configuration
channel.queue_declare(
    queue='orders',
    durable=True,
    arguments={
        'x-dead-letter-exchange': 'dlx',
        'x-dead-letter-routing-key': 'orders.failed',
        'x-message-ttl': 86400000,  # 24 hour TTL
        'x-max-length': 100000       # Max 100K messages
    }
)

Messages are dead-lettered when:

  1. Consumer rejects with requeue=False
  2. Message TTL expires
  3. Queue max-length exceeded
Try It: Dead Letter Queue Simulator

Adjust the message TTL, queue max-length, and consumer failure rate to see how messages flow between the main queue and the dead letter queue. Watch how different failure scenarios affect message loss and recovery.

Broker BexCheckpoint: failure routing

You now know:

  • A basic_nack(..., requeue=False) moves a failed message to the DLX when x-dead-letter-exchange is configured.
  • TTL and queue max-length are also dead-letter triggers; the example uses a 24 hour TTL and a 100K message cap.
  • The DLQ simulator treats a rate above 5% as an investigation signal, not as normal backlog.

Once rejected work is preserved, monitoring asks whether the healthy path is keeping up. Queue depth, consumers, rates, and unacked messages become action thresholds.

15.12 Monitoring and Alerting

Monitor the broker as a flow rather than as isolated counters. Begin with queue depth: warn above 50% of capacity, treat above 80% as critical, and scale consumers when sustained ingress outruns drain. Then check consumer count; fewer than two warrants a warning and zero requires an on-call alert because no worker can reduce the backlog.

Next compare message rate with tested capacity. Warn above 80%, treat above 95% as critical, and throttle publishers before headroom disappears. Read unacknowledged messages beside that rate: more than 1,000 is a warning and more than 5,000 is critical here, prompting a consumer-health check rather than automatic publisher scaling. Finally, inspect the dead-letter rate; above 1% warrants review and above 5% requires failure investigation. These example thresholds belong to this chapter’s sizing model and must be replaced with load-tested service limits before release.

RabbitMQ Management API Example:

import requests

def check_queue_health(host: str, queue: str) -> dict:
    """Check queue health via RabbitMQ Management API."""
    url = f"http://{host}:15672/api/queues/%2F/{queue}"
    response = requests.get(url, auth=('guest', 'guest'))

    data = response.json()
    return {
        'messages': data['messages'],
        'consumers': data['consumers'],
        'message_rate': data.get('message_stats', {}).get('publish_details', {}).get('rate', 0),
        'ack_rate': data.get('message_stats', {}).get('ack_details', {}).get('rate', 0)
    }
Try It: AMQP Health Monitor Dashboard

Simulate a production AMQP broker by adjusting queue depth, consumer count, and message rates. The dashboard applies the warning and critical thresholds from the monitoring table above and shows which alerts would fire.

Broker BexCheckpoint: broker health

You now know:

  • Queue depth above 50% is a warning and above 80% is critical, so depth is a capacity signal before it is an outage.
  • Consumer count below 2 removes redundancy, and 0 active consumers is an immediate on-call alert.
  • Unacked messages above 1000 warn and above 5000 are critical because they reveal slow or stuck consumers.

Those alerts identify symptoms; the calculators translate them into sizing decisions for rates, memory, processing time, and prefetch.

15.13 Continue to the Next Part

Carry this evidence into AMQP Operations: Capacity Tools and Diagnostics, which begins with Interactive Calculators.