Messaging Β· 08

Reliability & Operations

Choosing a broker is the easy part. This page is about the day after: which numbers tell you the truth, how to alert without crying wolf, how to redrive a dead-letter queue without making it worse, how to restart and upgrade without downtime, how to secure and recover the platform, and four decision-tree playbooks for the incidents you will actually get paged for.

12
Chapters
4
Playbooks
3
Brokers covered
2026
Docs verified
01
Chapter One - Signals

What to Watch, and Why

A broker can be perfectly healthy while the system on top of it is broken: the cluster is up, CPU is idle, and an order has been sitting unprocessed for two hours because one consumer is stuck. Monitoring a messaging system means watching the path of a message, not only the servers. Page 01 introduced lag and backpressure as ideas; this page turns them into numbers, alerts and runbooks.

Where to put a sensor: five points on the path of a message
Producer Can I write? Broker Is it safe and healthy? Consumer Is it being processed? record-error-rate record-retry-rate request-latency-avg buffer-available-bytes (client JMX) UnderReplicatedPartitions OfflinePartitionsCount IsrShrinksPerSec RequestHandlerAvgIdlePercent disk used per log dir records-lag-max last-poll-seconds-ago rebalance-rate-per-hour failed-rebalance-total age of the oldest unprocessed Same four questions on other brokers: RabbitMQ: messages_ready, messages_unacknowledged, consumer capacity, memory and disk alarms. SQS: NumberOfMessagesSent, ApproximateAgeOfOldestMessage, ApproximateNumberOfMessagesVisible, DLQ depth.

Every useful signal answers one of four questions. If a metric on your dashboard answers none of them, it is decoration.

QuestionWhat a bad answer meansKafkaRabbitMQSQS
Can producers write?Data is being refused or delayed at the sourceProducer record-error-rate, record-retry-rate; broker UnderMinIsrPartitionCountPublisher connections blocked by an alarm; publish confirm latencyNumberOfMessagesSent drops while the app still runs; API throttling errors in the app
Is the data safe?One more failure loses acknowledged messagesUnderReplicatedPartitions, OfflinePartitionsCount, ISR shrink and expand rateQuorum queue members online (check_if_node_is_quorum_critical)Managed by AWS: watch the DLQ instead
Is the data being consumed?Work is waiting; users feel it as delayConsumer lag (offsets and time)messages_ready and consumer capacityApproximateAgeOfOldestMessage, ApproximateNumberOfMessagesVisible
Is the platform healthy?You are about to lose the first three answersRequest handler idle, disk usage, produce and fetch latencyMemory alarm, free-disk alarmThrottling, KMS or IAM errors in consumers
Two rules that shape everything else
  • Alert on symptoms, investigate with causes. "Oldest message is 10 minutes old" is a symptom a human must act on. "Request handler idle is 0.25" is a cause: put it on a dashboard and as a warning, not as a 3 a.m. page.
  • Every alert needs a first action. If the on-call person cannot say what they would check or run when it fires, the alert is not ready. The playbooks in chapter 10 give each page-worthy symptom a first command.
Key insight: A courier company does not measure success by whether the trucks are running. It measures how long a parcel has been waiting. Trucks (brokers) are causes; waiting time (lag, age) is the symptom customers experience.
02
Chapter Two - Kafka

Kafka Metrics That Matter

Kafka brokers and the Java clients expose metrics through JMX. The table lists the ones worth a dashboard panel, with the MBean name from the Apache Kafka 4.0 monitoring documentation and what a bad value points at. "Normal" values are the ones the documentation states; where it states none, you establish a baseline for your own cluster.

SignalMBean or toolNormalIf it is off, look at
Under-replicated partitionskafka.server:type=ReplicaManager,name=UnderReplicatedPartitions0A broker down or slow, a saturated network or disk, a follower that cannot keep up
Under min-ISR partitions...ReplicaManager,name=UnderMinIsrPartitionCount0Writes with acks=all to these partitions are being rejected. This is the pager metric for producers
At min-ISR partitions...ReplicaManager,name=AtMinIsrPartitionCount0One more replica failure and you become under-min-ISR: an early warning
Offline partitionskafka.controller:type=KafkaController,name=OfflinePartitionsCount0No leader at all: partition unavailable for reads and writes. Page immediately
Active controller...KafkaController,name=ActiveControllerCount1 across the cluster0 or more than 1 in total means the control plane is unhealthy (KRaft quorum, chapter 05 of page 02)
ISR shrink / expand rate...ReplicaManager,name=IsrShrinksPerSec and IsrExpandsPerSec0 except around broker restartsRepeated shrink/expand pairs with no restart mean flapping followers: slow disks, GC pauses, network trouble
Unclean electionskafka.controller:type=ControllerStats,name=UncleanLeaderElectionsPerSec0Data was lost to keep a partition available (only possible if you enabled it)
Request handler idlekafka.server:type=KafkaRequestHandlerPool,name=RequestHandlerAvgIdlePercentIdeally above 0.3Broker threads are saturated: too many partitions, heavy compression, slow disk, or too many clients
Network processor idlekafka.network:type=SocketServer,name=NetworkProcessorAvgIdlePercentIdeally above 0.3Network threads busy: many connections, large messages, TLS cost
Produce / fetch latencykafka.network:type=RequestMetrics,name=TotalTimeMs,request=Produce (also FetchConsumer, FetchFollower)Your baselineThe total is broken into queue, local, remote and response-send time. A big remote part on Produce means waiting for followers (acks=all)
Follower lagkafka.server:type=ReplicaFetcherManager,name=MaxLag,clientId=ReplicaProportional to the maximum produce batch sizeFollowers behind the leader: precedes ISR shrinks
Disk usageOS metrics per log directory; kafka-log-dirs.sh --describeBelow your alert threshold with room for retentionRetention too long, a hot partition, a stuck cleaner. A full disk takes a broker down (chapter 4)
Consumer lag has two units

Lag in offsets is what Kafka tools print: log end offset minus the group's committed offset. It is easy to read and almost useless for urgency on its own, because 50,000 messages of lag is a few seconds on a topic taking 20,000 messages per second and a day on one taking 2 per second.

Lag in time is how old the record at the consumer's position is: now - timestamp of the record being processed. That is what a user experiences as delay. Kafka does not publish it as a single broker metric. You derive it: either the consumer records now - record.timestamp() as it processes (cheap, accurate, and a good SLO input), or a lag exporter estimates it from offsets and their timestamps. Dashboard both: offsets show how much, time shows how bad.

Shell - inspect a consumer group (output abridged / illustrative, shape from the Kafka docs)
$ bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group billing GROUP TOPIC PARTITION CURRENT-OFFSET LOG-END-OFFSET LAG CONSUMER-ID HOST CLIENT-ID billing orders 0 241019 395308 154289 consumer-1-029af89c /10.0.1.17 consumer-1 billing orders 1 241018 398817 157799 consumer-1-029af89c /10.0.1.17 consumer-1 billing orders 2 243655 243656 1 consumer-2-42c1abd4 /10.0.1.18 consumer-2 $ bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group billing --state

Read it by column. All partitions lagging points at slow processing or too few consumers. One partition lagging points at a hot key or a stuck consumer holding that partition. Lag that grows only right after a restart points at a rebalance (chapter 6). --state shows whether the group is stable or rebalancing.

Broker health from the command line
Shell - quick checks (each prints nothing when the cluster is healthy)
$ bin/kafka-topics.sh --bootstrap-server localhost:9092 --describe --under-replicated-partitions $ bin/kafka-topics.sh --bootstrap-server localhost:9092 --describe --under-min-isr-partitions $ bin/kafka-topics.sh --bootstrap-server localhost:9092 --describe --unavailable-partitions $ bin/kafka-log-dirs.sh --bootstrap-server localhost:9092 --describe

The --under-replicated-partitions, --under-min-isr-partitions and --unavailable-partitions options of kafka-topics.sh --describe filter the output to just the partitions in trouble, which makes them good first commands in an incident and good health checks before the next step of a rolling restart (chapter 7).

Client-side metrics you own
  • Producer: record-error-rate and record-retry-rate (are sends failing or being retried), request-latency-avg, and buffer-available-bytes (when the buffer is exhausted, send() blocks up to max.block.ms, default 60 s, which looks like a hung request thread).
  • Consumer: records-lag-max, last-poll-seconds-ago compared to max.poll.interval.ms (default 5 minutes: a consumer that does not poll in time is removed from the group), and in the coordinator metrics rebalance-rate-per-hour and failed-rebalance-total.
  • These live in your application process, so export them through your metrics library (Micrometer exposes the Kafka client metrics for Spring apps, page 04).
⚠ Paging on every URP blip
  • UnderReplicatedPartitions is above 0 for the whole time a broker restarts, by design. An alert that fires immediately will page during every deploy and train people to ignore it. Alert when it stays above 0 for longer than a normal restart plus catch-up (choose from your own measurements), and alert immediately only on UnderMinIsrPartitionCount and OfflinePartitionsCount.
03
Chapter Three - RabbitMQ and SQS

The Same Questions on Other Brokers

RabbitMQ

RabbitMQ reports queue and node state through the management HTTP API and the Prometheus plugin. The queue counters to know, using the names from the RabbitMQ monitoring documentation (4.x):

SignalNameWhat it tells you
Ready messagesmessages_ready (cluster-wide: queue_totals.messages_ready)Messages waiting for a consumer. Growth means producers are faster than consumers, or there are no consumers
Unacknowledged messagesmessages_unacknowledgedDelivered but not yet acked. A high, flat value means consumers are slow or stuck holding deliveries; a very large one often means the prefetch is too high (page 05)
Consumer capacityShown per queue in the management UI and Prometheus data (previously called consumer utilisation)Fraction of time the queue could deliver immediately. Below 100% suggests more consumers, faster handling or a higher prefetch would help. The docs call it a hint: confirm with your own consumer metrics
Memory alarmmem_alarm per nodeMemory is above the watermark (vm_memory_high_watermark, default 0.6 of RAM). Publishing connections are blocked
Free-disk alarmdisk_free_alarm; limit disk_free_limitFree disk is below the limit (default 50 MB, far too low for production: set it relative to RAM). Publishing connections are blocked

The two alarms deserve emphasis because of what they do to producers. While either alarm is active, RabbitMQ stops reading from connections that publish, so producers see their calls hang rather than fail, and consumer-only connections keep working. A queue that is growing because of slow consumers can therefore turn into a publisher outage when memory or disk runs out. Alert on the alarm itself, and on queue growth long before it.

Shell - RabbitMQ checks
$ rabbitmqctl list_queues name messages_ready messages_unacknowledged consumers $ rabbitmq-diagnostics check_if_node_is_quorum_critical $ rabbitmq-diagnostics alarms

check_if_node_is_quorum_critical exits non-zero if stopping this node would cost any quorum queue, stream or internal component its online quorum. It is the gate before restarting a node (chapter 7).

Amazon SQS

SQS publishes CloudWatch metrics in the AWS/SQS namespace with a single dimension, QueueName. These are the ones that matter:

MetricMeaningUse it for
ApproximateAgeOfOldestMessageAge in seconds of the oldest unprocessed message; reported only if the queue has at least one active messageThe primary alarm. It is the delay a user is experiencing, whatever the volume
ApproximateNumberOfMessagesVisibleMessages available for retrieval right nowBacklog size, autoscaling input, DLQ depth (any value above 0 on a DLQ)
ApproximateNumberOfMessagesNotVisibleIn-flight: received but not yet deleted or expiredStuck or slow consumers: a growing in-flight count with no deletes. In-flight messages count against a quota (see the SQS quotas page)
ApproximateNumberOfMessagesDelayedMessages held by a delay queue or per-message delayConfirming that "missing" messages are merely delayed
NumberOfMessagesSent, NumberOfMessagesReceived, NumberOfMessagesDeletedThroughput countersReceived much greater than deleted means messages are being redelivered (failures or a visibility timeout that is too short)
NumberOfEmptyReceivesReceiveMessage calls that returned nothingTuning polling (long polling) and API cost
Two spellings of the same thing

GetQueueAttributes returns ApproximateNumberOfMessages, ApproximateNumberOfMessagesNotVisible and ApproximateNumberOfMessagesDelayed. CloudWatch calls the first one ApproximateNumberOfMessagesVisible. They mean the same; use the right name for the tool you are in.

Because SQS metrics are only reported while the queue is active, an empty queue can show missing data instead of zero. Alarms on these metrics should set how missing data is treated (notBreaching for age and depth alarms is the usual choice) or they will sit in INSUFFICIENT_DATA exactly when everything is fine.

04
Chapter Four - Alerting

Alert Design, Dashboards and the Capacity Loop

Most messaging pages at 3 a.m. are noise, because they are based on a number with no relationship to harm. The rule is simple and worth repeating in code review: alert on how long a message waits, not on how many are waiting.

Same depth, very different urgency: why age beats depth
Queue A: 10,000 messages, consumers keep up oldest message: 4 seconds old throughput 3,000 per second: backlog clears in about 3 seconds Queue B: 10,000 messages, consumers stuck oldest message: 3 hours old throughput 0 per second: backlog never clears by itself A depth threshold treats both the same. An age threshold separates them. depth > 5,000 fires for A (false alarm) depth > 5,000 fires for B (right, by luck) age > 5 minutes fires for B only
AlertCondition (example, tune to your SLO)SeverityFirst action
Messages too oldSQS ApproximateAgeOfOldestMessage above your processing SLO for several consecutive minutes; Kafka time-lag above SLOPagePlaybook A (chapter 10): is the consumer running, slow, or rebalancing?
DLQ not emptyDLQ visible messages above 0 (SQS), dead-letter topic receiving records, RabbitMQ DLQ messages_ready above 0Ticket, page if the rate is highPlaybook B: inspect before redriving
Data at riskKafka UnderMinIsrPartitionCount or OfflinePartitionsCount above 0PagePlaybook C
Replication degradedURP above 0 for longer than a normal restart; ISR shrinks with no restartWarningFind the slow or missing broker
Broker saturatedRequest handler or network idle below 0.3 for a sustained periodWarningCapacity loop, below
Resource alarmRabbitMQ memory or disk alarm activePage (producers are blocked)Free memory or disk, find the growing queue
Disk fillingFree space on a Kafka log dir or RabbitMQ data dir below a threshold, or predicted to run out within hoursPage when hours remain, not minutesLower retention, add storage or brokers
Rebalance stormrebalance-rate-per-hour high for a groupWarningChapter 6
An alarm you can actually create

This CloudWatch alarm fires when the oldest message in the queue orders has been older than five minutes for five consecutive one-minute periods, and stays quiet when the queue is empty and the metric has no data. The flags are those of aws cloudwatch put-metric-alarm; the SNS topic ARN is a placeholder.

Shell - age alarm for an SQS queue
aws cloudwatch put-metric-alarm \ --alarm-name orders-oldest-message-age \ --namespace AWS/SQS \ --metric-name ApproximateAgeOfOldestMessage \ --dimensions Name=QueueName,Value=orders \ --statistic Maximum \ --period 60 \ --evaluation-periods 5 \ --datapoints-to-alarm 5 \ --threshold 300 \ --comparison-operator GreaterThanThreshold \ --treat-missing-data notBreaching \ --alarm-actions arn:aws:sns:us-east-1:123456789012:oncall
What a good dashboard shows
  • Top row, per important queue or consumer group: age or time-lag, depth or offset lag, consume rate versus produce rate, DLQ depth. This row answers "is anything late?" in five seconds.
  • Second row, per cluster: under-min-ISR and offline partitions, URP, ISR shrink rate, request handler idle, produce and fetch latency, disk used. This row answers "is the platform why?".
  • Third row, per consumer app: processing time, error rate, retries, rebalance count, JVM pauses. Often the real cause lives here, not in the broker.
  • Put produce rate and consume rate on the same chart. The gap between the lines is the backlog growing or shrinking, and it is the quickest way to see whether a backlog will drain.
The capacity planning loop

Capacity is not a one-time sizing exercise (page 02, chapter 09 gave the starting arithmetic). It is a loop you run on a schedule and after every large change in traffic.

Measure
β–Ά
Peak and average produce rate, message size, consumer processing time per message, current disk and CPU headroom. Use real peaks, including the daily and seasonal ones
Model
β–Ά
Needed consumers = peak arrival rate x processing time per message / concurrency per consumer. Disk = bytes per second x retention x replication factor, plus headroom
Add headroom
β–Ά
Plan to run well below saturation at peak so that losing one broker or one zone does not push the rest over the edge (with RF 3 across 3 brokers, two remaining brokers must carry all the traffic)
Test
β–Ά
Load-test the planned size, not only the current one (chapter 9)
Alert on the trend
β–Ά
Alert on "disk full in N days" and "consumers at 80% busy", then go back to Measure

A worked example, with made-up numbers for illustration: a consumer takes 40 ms per message, one thread, and peak arrival is 600 messages per second. One thread handles 25 messages per second, so you need at least 24 concurrent threads at peak, and with a safety margin of 50% about 36. On Kafka that also means at least 36 partitions for that topic, because one partition feeds only one consumer in a group. On SQS the same arithmetic gives the number of concurrent pollers; on RabbitMQ, consumers times prefetch.

⚠ Autoscaling on depth alone
  • Scaling consumers when depth is high can add load to an already struggling database, which makes processing slower, which grows the queue, which scales more consumers. Scale on age or on backlog-per-consumer, cap the maximum, and make sure the dependency can absorb the extra concurrency.
05
Chapter Five - Dead Letters

DLQ Operations, Redrive and the Poison Message

Page 01 explained why a bounded retry followed by a dead-letter queue keeps one bad message from blocking the lane. A DLQ is not a solution; it is a parking lot for incidents, and it needs an owner, an alert and a procedure. Chapter 4 set the alert. This chapter is the procedure.

Safe redrive: never send the backlog back to a consumer that is still broken
1 Alert DLQ depth above 0 2 Inspect read a sample, find the cause 3 Fix deploy the code or fix the data 4 Test redrive 1 to 10 messages first 5 Redrive with a rate limit watch consumer errors and the age of the source queue while it runs 6 Verify and close DLQ depth reaches 0, no new failures, idempotent consumers absorbed repeats Why each step exists: Inspect: redriving unread messages repeats the failure. Fix first: or they return to the DLQ. Small batch: proves the fix with 10 messages, not 10,000. Rate limit: the backlog arrives on top of live traffic. Idempotency: redriven messages may already have had a partial effect.
SQS: inspect and redrive

An SQS dead-letter queue must be in the same account and Region as its source queue. For a standard queue the original enqueue timestamp is kept when a message moves to the DLQ, and expiry is computed from it, so give the DLQ a longer retention period than the source queue or messages can expire in the DLQ before anyone looks at them.

Shell - look at a few messages without removing them
$ aws sqs receive-message \ --queue-url "$DLQ_URL" \ --max-number-of-messages 5 \ --attribute-names All \ --message-attribute-names All \ --visibility-timeout 30

Received messages become invisible for the visibility timeout and then reappear, so this is a safe peek as long as you do not delete them. Read the body and the attributes (ApproximateReceiveCount, SentTimestamp) to understand the failure.

When the fix is deployed, use the message move task (the API behind the console's "Start DLQ redrive"). It runs asynchronously in the background, moves messages from the DLQ to the original source queue by default or to another queue of the same type, and takes a maximum rate.

Shell - redrive with a rate limit (SQS StartMessageMoveTask)
$ aws sqs start-message-move-task \ --source-arn arn:aws:sqs:us-east-1:123456789012:orders-dlq \ --destination-arn arn:aws:sqs:us-east-1:123456789012:orders \ --max-number-of-messages-per-second 5 $ aws sqs list-message-move-tasks \ --source-arn arn:aws:sqs:us-east-1:123456789012:orders-dlq $ aws sqs cancel-message-move-task --task-handle "$TASK_HANDLE"
  • The documented maximum custom rate is 500 messages per second, and the guidance is to start with a small value and increase it while watching the source queue.
  • Only one active move task per queue is supported, a task can run for at most 36 hours, and you cannot filter or modify messages during a redrive. To fix a message before replay you must read it, correct it and send it yourself.
  • Redriven messages are new messages: they get a new message ID and enqueue time, and the retention period starts again. For FIFO queues, redriven messages interleave with new traffic from producers, so strict order across the replay is not guaranteed.
  • Cancelling a running task leaves already-moved messages in the destination.
Kafka and RabbitMQ

Kafka has no built-in DLQ. Your consumer, or Spring Kafka's dead-letter publishing recoverer (page 04), writes the failed record plus failure headers to a dead-letter topic. Redrive is a small consumer on that topic that republishes to the original topic. Because the original is a log, you can also fix the bug and reset the group's offsets instead (kafka-consumer-groups.sh --reset-offsets, which only prints the plan until you add --execute; the group must be stopped), which replays everything and is only safe for idempotent consumers.

RabbitMQ uses dead-letter exchanges (page 05). Quorum queues also count failed deliveries and, since RabbitMQ 4.0, dead-letter or drop a message after a delivery limit that defaults to 20, exposed in the x-delivery-count header (the limit is set by the delivery-limit policy argument; the 3.13 behaviour of no limit is restored with -1). Redrive from a DLX queue is done with a shovel or a small script publishing back to the source exchange.

Shell - hold the consumer before resetting offsets
$ bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 \ --group billing --topic orders --reset-offsets --to-datetime 2026-10-31T09:00:00.000 # prints the proposed new offsets only; add --execute after reviewing them
The redrive checklist
StepQuestionIf the answer is no
Cause knownDo you know why they failed (code bug, bad data, dependency outage)?Do not redrive. Keep inspecting
Fix deployedIs the fixed consumer the one that will receive the replay?Deploy it first, or the messages return to the DLQ and the retry budget is spent
Dependency healthyIf the cause was a downstream outage, is it up and able to take the backlog on top of live traffic?Wait, or redrive at a very low rate
IdempotentCan every handler tolerate a message it may have partially processed before?Add a dedup check (page 01, chapter 05) before replaying
Expiry safeAre any messages stale (an expired coupon, a cancelled order)?Discard or handle them separately: replay is not always correct
Rate-limited and watchedIs the replay rate bounded, and is someone watching consumer errors and age?Start at 1 to 10 messages per second, then ramp
The poison message playbook

A poison message is one that always fails. Its damage depends on the broker: on Kafka it blocks its partition (everything behind it waits), on a queue it burns retries and CPU and can starve the others. The symptoms are a stuck offset on one partition with the consumer alive, or a message with a very high delivery count.

  • Detect: one partition with lag growing while the others are fine and the consumer logs the same exception with the same offset; SQS messages with a high ApproximateReceiveCount; RabbitMQ messages with a growing x-delivery-count.
  • Contain: make sure retries are bounded and non-retryable errors (deserialization, validation) go to the DLQ immediately, not after the full retry schedule. This is a code and configuration property you set before the incident.
  • Unblock (Kafka): the cleanest fix is the dead-letter publish above. In an emergency, with the consumer stopped, advance that partition's offset by one with --reset-offsets --shift-by 1 for the specific topic and partition, after saving the record somewhere. That is data loss for that record: write it down in the incident log.
  • Prevent: validate on the producer side, version your schemas, and test consumers against malformed input (chapter 9).
06
Chapter Six - Stability

Rebalance Storms, Backpressure and Load Shedding

Rebalance storms

Every time group membership changes, partitions are reassigned and, with the classic protocol, processing stops for the affected consumers while it happens. One rebalance is normal. A storm is rebalances back to back: lag grows, throughput collapses, and the system looks "up" while doing no work.

The rebalance storm: a loop that feeds itself
Consumer is slow or pauses (GC, big batch) No poll within max.poll.interval.ms Member removed, group rebalances Partitions move; processing pauses, uncommitted work repeats Backlog grows, next batch is bigger Even slower. Another member is removed. repeat Common triggers: slow processing, rolling deploys that restart every pod, autoscaler flapping, session.timeout.ms or max.poll.interval.ms set too tight, long GC pauses, network blips.
CauseWhat you seeFix
Processing exceeds max.poll.interval.ms (default 5 minutes)Consumer logs say it was removed from the group; commit failures; repeated processing of the same batchLower max.poll.records (default 500) so a batch fits in the interval, move slow work off the poll thread carefully, or raise the interval only if the work is genuinely long
Heartbeats missed beyond session.timeout.ms (default 45 s; heartbeat.interval.ms default 3 s)Members flapping during GC pauses or network blipsFix the pauses first; do not just raise the timeout, which also delays detection of real failures
Rolling deploy replaces every consumerOne rebalance per restarted pod, clustered togetherUse static membership (group.instance.id) so a restarted consumer rejoining within the session timeout keeps its partitions, and deploy gradually
Autoscaler adds and removes consumersRebalance rate follows the scaling graphAdd stabilisation windows; scale less often and in larger steps
Eager assignment moves everythingAll consumers pause on every changeUse the CooperativeStickyAssignor so only moved partitions pause. The 4.0 default partition.assignment.strategy lists RangeAssignor first, then CooperativeStickyAssignor, so a group keeps using the eager Range protocol until Range is removed from every member (two rolling restarts)

Kafka 4.0 also made the new consumer group protocol (KIP-848, group.protocol=consumer) generally available. It moves assignment to the broker and removes the global "stop the world" synchronisation of the classic protocol, so rebalances stop pausing unaffected consumers. The default is still classic in 4.0, so you opt in per consumer, and some classic settings such as session.timeout.ms are not supported with it. Test before adopting it.

Backpressure: when the consumer cannot keep up

Page 01 defined backpressure as the consumer slowing the producer down. In a real system you pick, in advance, which of three things gives way when load exceeds capacity. If you do not choose, the system chooses for you, usually with an outage.

StrategyHowCostsGood for
Buffer (absorb)Let the queue or log grow. Add consumers. This is why brokers existLatency grows; storage and retention limits become the ceilingBursts shorter than the time to scale up
Slow the producerBounded buffers that block or reject (Kafka buffer.memory, RabbitMQ publisher blocking by alarm, a rate limit at the API edge)The user-facing request waits or gets an errorWhen accepting work you cannot finish is worse than refusing it
Shed loadDrop or degrade lower-value work: sampling, dropping stale messages, skipping enrichmentSome work is lost on purposeMetrics, telemetry, recommendations, and anything with a freshness deadline
  • Set a maximum useful age. A "send the verification code" message that is 20 minutes old is worse than none. Put a TTL on it (SQS retention or per-message TTL in RabbitMQ), or have the consumer check the timestamp and drop it.
  • Prioritise by queue, not by hoping. Separate queues or topics for "payments" and "newsletters" so a flood of one cannot starve the other. Kafka has no per-message priority; separate topics is the idiom.
  • Make the limit explicit. A consumer with max.poll.records, a bounded thread pool and a bounded in-memory queue applies backpressure by design. An unbounded in-memory queue just moves the failure into your heap.
07
Chapter Seven - Change Safely

Rolling Restarts and Upgrades

Restarts are where well-designed clusters get hurt, because they are routine and people do them quickly. The principle for every broker is the same: change one node, wait until the system has fully recovered, then change the next, and make "fully recovered" a command that returns a value, not a feeling.

Rolling restart of a three-broker Kafka cluster: one at a time, gate on URP
Broker 1 1 controlled shutdown 2 change or upgrade, start 3 wait: URP = 0 again Broker 2 1 controlled shutdown 2 change or upgrade, start 3 wait: URP = 0 again Broker 3 1 controlled shutdown 2 change or upgrade, start 3 wait: URP = 0 again The gate before every next step: kafka-topics.sh --describe --under-replicated-partitions prints nothing. If it never clears, stop. Restarting the next broker now could leave partitions with too few replicas in sync, and with min.insync.replicas=2 that rejects producer writes (chapter 10, playbook C). After all brokers: finalize the upgrade (kafka-features.sh), then optionally rebalance leadership.
Kafka
  • Before starting: the cluster is healthy: no under-replicated, under-min-ISR or offline partitions, and no reassignment in progress. Read the upgrade notes for the target version.
  • Stop gracefully. With controlled.shutdown.enable=true the broker syncs its logs to disk (fast restart, no log recovery) and migrates partition leadership to other replicas before stopping, which keeps each partition unavailable only for a few milliseconds. Controlled shutdown succeeds only if every partition on the broker has another live replica, which is one more reason for replication factor 3.
  • Do not hard-kill (kill -9) a broker unless it is hung: it skips both optimisations, and it leaves followers to time out of the ISR.
  • Start it and wait. The broker rejoins as a follower and catches up. Wait until under-replicated partitions are back to 0 before touching the next broker.
  • Restore leadership balance (optional). auto.leader.rebalance.enable (default true) moves leaders back to their preferred replicas over time; kafka-leader-election.sh --election-type preferred --all-topic-partitions does it immediately.
  • Finish an upgrade. After all brokers run the new version and you have verified behaviour, finalize with kafka-features.sh upgrade --release-version 4.0 (the Kafka 4.0 upgrade notes' example). Upgrade clients separately and one at a time; clients must be on 2.1 or higher before a 4.0 broker upgrade.
Shell - the loop for one broker (Kafka 4.0, KRaft, systemd assumed)
$ bin/kafka-topics.sh --bootstrap-server broker1:9092 --describe --under-replicated-partitions # must print nothing before you start $ ssh broker2 'sudo systemctl stop kafka' # SIGTERM triggers controlled shutdown $ ssh broker2 '...apply the change, then...' && ssh broker2 'sudo systemctl start kafka' $ until [ -z "$(bin/kafka-topics.sh --bootstrap-server broker1:9092 --describe --under-replicated-partitions)" ]; do sleep 10 done # only now move on to broker3

Give systemctl a long enough stop timeout: a broker with many partitions needs time to finish controlled shutdown, and a service manager that kills it after 90 seconds defeats the point. In KRaft clusters with separate controller nodes, restart the controller quorum the same way, one node at a time, and never take down enough controllers to lose the quorum majority.

RabbitMQ with quorum queues

RabbitMQ's rolling upgrade is also node by node, and the dangerous part is the same: quorum. A quorum queue stays available only while a majority of its replicas are online. In a 3-node cluster, restarting node B while node A is still down leaves some queues with one of three replicas and no quorum.

  • Gate: run rabbitmq-diagnostics check_if_node_is_quorum_critical against the node you are about to stop. A non-zero exit means stopping it would cost some queue, stream or internal component its quorum. When automating, rabbitmq-upgrade await_online_quorum_plus_one blocks the shutdown until enough nodes are up.
  • Maintenance mode: rabbitmq-upgrade drain transfers queue primaries off the node and stops it accepting client connections; rabbitmq-upgrade revive brings it back to regular duty.
  • Pre-checks from the RabbitMQ upgrade guide: no alarms in effect, no ongoing queue or stream replica synchronisation, all stable feature flags already enabled, and a supported rolling-upgrade path between the two versions. After the last node, enable the new stable feature flags.
  • Leader balance: after a rolling upgrade, queue leaders are unevenly spread. rabbitmq-queues rebalance quorum redistributes them.
Shell - RabbitMQ node maintenance (RabbitMQ 4.x)
$ rabbitmq-diagnostics check_if_node_is_quorum_critical $ rabbitmq-upgrade drain # stop the node, upgrade the package or Erlang, start the node $ rabbitmq-upgrade revive $ rabbitmq-queues rebalance quorum
⚠ Restarting everything at once because it is "just a config change"
  • A parallel restart of all brokers or all nodes drops availability to zero and, on Kafka, can leave partitions with unclean state. A config change that needs a restart is still a rolling restart. Automate the gate (URP is 0, quorum is not critical) so the safe path is also the easy path.
08
Chapter Eight - Security

Who Can Read, Write and Administer

A broker holds the facts of your business in motion: orders, payments, personal data. Three controls are required for any shared or production broker: encryption in transit (TLS), authentication (who are you?) and authorisation (what may you do?). Everything below applies equally to a laptop broker you later promote to a shared one, so set it up once, early.

Kafka (4.0)RabbitMQ (4.x)AWS (SQS, SNS)
Encryption in transitTLS on listeners (SSL or SASL_SSL); broker ssl.keystore.location, ssl.truststore.location; clients ssl.truststore.locationTLS listener, conventionally port 5671: listeners.ssl.default, ssl_options.cacertfile, certfile, keyfileHTTPS endpoints; enforce with a policy condition on aws:SecureTransport
AuthenticationSASL mechanisms: PLAIN, SCRAM-SHA-256/512, GSSAPI (Kerberos), OAUTHBEARER; or mutual TLS (ssl.client.auth=required)Users per node set: rabbitmqctl add_user; also x509 certificates and other backends via pluginsIAM principals and SigV4 request signing; no broker passwords
AuthorisationACLs through StandardAuthorizer; kafka-acls.shPer-virtual-host configure / write / read permission patternsIAM identity policies and queue / topic resource policies
IsolationPrefixed ACLs and topic naming per team; quotasVirtual hosts (rabbitmqctl add_vhost)Separate queues and accounts; least-privilege per queue ARN
Encryption at restDisk or volume encryption (the broker does not encrypt log files itself)Disk or volume encryptionSSE-SQS or SSE-KMS (customer-managed key) per queue
Kafka

Use SASL_SSL so credentials and data are both encrypted: SCRAM, for instance, must only be used over TLS, as the Kafka documentation stresses. Enable the authorizer and deny by default: the broker setting allow.everyone.if.no.acl.found=true makes a resource with no ACLs open to everyone, which is the wrong default for production.

Broker - TLS plus SCRAM, StandardAuthorizer (server.properties, shape from the Kafka docs)
listeners=SASL_SSL://0.0.0.0:9093 security.inter.broker.protocol=SASL_SSL sasl.mechanism.inter.broker.protocol=SCRAM-SHA-256 sasl.enabled.mechanisms=SCRAM-SHA-256 ssl.keystore.location=/var/private/ssl/server.keystore.jks ssl.truststore.location=/var/private/ssl/server.truststore.jks authorizer.class.name=org.apache.kafka.metadata.authorizer.StandardAuthorizer allow.everyone.if.no.acl.found=false super.users=User:admin

Keystore and key passwords are omitted on purpose: load them from a secrets store, never from a committed file. Grant each service the least it needs with the convenience options --producer and --consumer, which expand to the operations those roles require (a consumer also needs a group):

Shell - least-privilege ACLs for one service
$ bin/kafka-acls.sh --bootstrap-server broker1:9093 --command-config admin.properties \ --add --allow-principal User:billing --consumer --topic orders --group billing $ bin/kafka-acls.sh --bootstrap-server broker1:9093 --command-config admin.properties \ --add --allow-principal User:checkout --producer --topic orders $ bin/kafka-acls.sh --bootstrap-server broker1:9093 --command-config admin.properties --list --topic orders
RabbitMQ
  • **Delete the default guest user** or restrict it; by default it can only connect from localhost, but it is a well-known credential.
  • One user per application, one virtual host per environment or tenant, with the narrowest permission patterns: rabbitmqctl set_permissions -p orders-prod billing "^billing\." "^billing\." "^orders\." (configure, write, read regular expressions on resource names).
  • Use TLS on port 5671 and, where possible, verify client certificates (ssl_options.verify = verify_peer, ssl_options.fail_if_no_peer_cert = true).
  • Do not publish the management UI to the internet; put it behind your SSO or a private network.
AWS
  • IAM, not credentials in config. Give each service a role that allows only sqs:ReceiveMessage, sqs:DeleteMessage, sqs:ChangeMessageVisibility (consumer) or sqs:SendMessage (producer) on that one queue ARN. Redrive needs additional permissions: see the "queue permissions for dead-letter queue redrive" section of the SQS guide.
  • Encrypt with SSE. SSE-SQS uses SQS-managed keys; SSE-KMS uses a KMS key you control, which adds auditability and key policy as a second access control, at the price of KMS calls and a dependency on key availability. Producers and consumers then need KMS permissions too, and a queue that uses the default AWS-managed key cannot be used by other accounts for some integrations (see the SSE page of the SQS guide).
  • Resource policies for fan-out: an SNS topic can only send to an SQS queue if the queue policy allows sns.amazonaws.com for that topic ARN. A missing statement is the commonest reason "the subscription is confirmed but nothing arrives".
⚠ Secrets in the message
  • Messages sit in logs, DLQs and replays for days. Never put credentials, full card numbers or unneeded personal data in a payload. Send an identifier and let the consumer fetch the sensitive record under its own authorisation, and treat the DLQ and any replay tooling as places that need the same access control as production data.
09
Chapter Nine - Resilience

Disaster Recovery and Testing Reliability

Disaster recovery and multi-region

Two numbers define the problem. RPO (recovery point objective) is how much data you can afford to lose; RTO (recovery time objective) is how long you can be down. Copying messages to another region is almost always asynchronous, so RPO is greater than zero: the last few seconds before the disaster may not have arrived. Decide whether that is acceptable before you buy complexity.

PlatformPatternNotes
KafkaA second cluster in another region fed by MirrorMaker 2 (part of Apache Kafka, built on Kafka Connect). Active/passive: one flow primary->secondary.enabled = true. Active/active: flows both waysMirrorMaker replicates topics, topic configurations, consumer groups with their offsets, and ACLs. Replicated topics are renamed with the source cluster alias as a prefix by default (for example us-west.foo-topic), which is how active/active avoids loops. Offsets differ between clusters, so consumers need translated offsets on failover. Exactly-once for MirrorMaker has been supported on dedicated clusters since 3.5.0
Kafka (single cluster)One cluster across three availability zones: RF 3, rack-aware replica placement (broker.rack), min.insync.replicas=2Survives the loss of a zone without data loss and without a second cluster. It does not survive losing the region
RabbitMQA cluster inside one region across zones with quorum queues; replicate between regions with a federation or shovel link, or at the applicationCross-region links are asynchronous. Avoid stretching one cluster over a high-latency WAN: quorum queues need low-latency links between members
SQSQueues are regional. Run an identical queue in a second Region and have producers or an upstream SNS topic publish to both, or fail over by pointing producers at the standbyA DLQ must be in the same Region and account as its source. Messages already in the lost Region's queue are unavailable until it recovers
SNSSupports cross-Region delivery to SQS queues and Lambda functions in a different RegionA cross-region fan-out topology is: publish once to an SNS topic, deliver to an SQS queue in each Region
  • Practise the failover. A DR plan that has not been run is a hypothesis. Run a game day at least once a year: promote the secondary, repoint a consumer, measure the time and the lost tail, and write down what surprised you.
  • Consumers must be idempotent to be safely repointed: after a failover a consumer replays from translated offsets that may be slightly behind where it really was.
  • Keep configuration as code so the secondary has the same topics, ACLs, DLQs and alarms as the primary, not a hand-built approximation.
Testing reliability

Reliability that has never been tested is a belief. Messaging systems fail in specific ways, so test those specific ways rather than only "does it work".

TestHowWhat it proves
Kill a consumer mid-messageTerminate the process after it received and before it acked or committedNo loss, and the redelivered message is handled idempotently
Kill a brokerStop one Kafka broker or RabbitMQ node during traffic (never all of them)Producers retry and recover; consumers continue; leaders move; URP returns to 0
Poison messagePublish a malformed record and an always-failing oneIt reaches the DLQ with metadata and does not block the lane
Duplicate deliverySend the same message twice, or force a redeliveryExactly one business effect
Slow dependencyAdd latency to the database the consumer usesBackpressure, retry limits and age alarms behave as designed; no rebalance storm
ReplayReset a group to an earlier time in a test environment (or redrive a DLQ)The system tolerates a replay and your runbook is correct
Load testGenerate peak (and 2x peak) traffic against the real sizeCapacity numbers from chapter 4 are right

For Kafka, kafka-producer-perf-test.sh generates load with a controlled rate and reports throughput and latency. In the 4.0 source its main options are --topic, --num-records, --record-size (or a payload file), --throughput (messages per second, required; -1 disables throttling), and --producer-props such as bootstrap.servers=... and acks=all. kafka-consumer-perf-test.sh is the consumer-side counterpart (--bootstrap-server, --topic, --messages).

Shell - load a test topic at a fixed rate, with production-like settings
$ bin/kafka-producer-perf-test.sh \ --topic orders-loadtest \ --num-records 600000 \ --record-size 1024 \ --throughput 2000 \ --producer-props bootstrap.servers=broker1:9092 acks=all enable.idempotence=true $ bin/kafka-consumer-perf-test.sh \ --bootstrap-server broker1:9092 --topic orders-loadtest --messages 600000
  • Test with **the same replication factor, acks, compression and message size as production**. A benchmark with acks=1 and tiny messages tells you nothing about your real limits.
  • Run the load test while killing a broker. Steady-state numbers are the easy part; the interesting number is throughput and latency during recovery.
  • Use a throwaway topic and delete it afterwards. Never load-test against a shared production topic.
  • For application-level chaos (kill, delay, drop) use whatever fault-injection tool your platform already has; the scenarios above matter more than the tool.
10
Chapter Ten - Incidents

Four Playbooks as Decision Trees

When the pager goes off nobody wants an essay. These four playbooks cover the incidents that account for most messaging pages. Each is a decision tree (symptom, then the check that splits the possibilities, the cause it reveals, and the fix) followed by a table of the exact commands. Keep them next to your alerts.

Playbook A: consumer lag is growing
Playbook A: lag or message age keeps growing
Alert: oldest message age above SLO or consumer lag rising check: is the consumer running and polling? last-poll-seconds-ago, logs Cause: crashed, stuck, or removed by repeated rebalances fix: restart or fix the stuck code; see chapter 6 (stable group, one owner each) check: produce rate vs consume rate; processing time per message; dependency latency Cause: consumers too slow: slow database or API, a bigger payload, a deploy fix: fix the dependency or scale consumers (up to the partition count); add headroom check: lag per partition (one partition or all?) Cause: one hot partition or a poison message blocking it while others are fine fix: poison: dead-letter it. Hot key: fix the key choice; never just add consumers Order of checks: running, then slow, then skewed. Most lag incidents end at the first or second box.
SymptomCheckCommand or fix
Lag on all partitions, group stableProcessing time and dependency healthkafka-consumer-groups.sh --describe --group G then app metrics; scale consumers or fix the slow call
Group keeps rebalancing--state, consumer logs, rebalance-rate-per-hourLower max.poll.records, static membership, steady autoscaling
One partition laggingWhich key dominates it, or which offset is stuckLook at the record at CURRENT-OFFSET; dead-letter a poison record; revisit the key
SQS age rising, in-flight highApproximateNumberOfMessagesNotVisible, deletes versus receivesVisibility timeout too short or handlers stuck: extend with ChangeMessageVisibility, fix handler
RabbitMQ messages_unacknowledged high and flatConsumers stuck holding deliveries; prefetch too largerabbitmqctl list_queues name messages_ready messages_unacknowledged consumers; restart the stuck consumer; lower prefetch
Playbook B: the dead-letter queue is filling
Playbook B: the DLQ is receiving messages
Alert: DLQ depth above 0 or dead-letter rate rising check: read 5 messages. Same exception? Same producer or version? Cause: a bug or a bad schema from a recent deploy (permanent failure) fix: roll back or fix; stop the bad producer; then redrive with the checklist (chapter 5) check: did a dependency fail around the first message timestamp? Cause: transient outage that exceeded the retry budget (maxReceiveCount too low?) fix: confirm dependency is healthy, redrive slowly; raise the retry budget/backoff check: NumberOfMessages Received vs Deleted; handler time vs visibility timeout Cause: visibility timeout shorter than processing, so messages exceed receive count fix: raise the timeout above p99 processing time, or extend it while working Never redrive before you know which box you are in. Redriving a bug just refills the DLQ.
SymptomCheckCommand or fix
SQS DLQ depth above 0Peek at messagesaws sqs receive-message --queue-url DLQ --max-number-of-messages 5 --attribute-names All --message-attribute-names All
Cause fixed, ready to replayChecklist in chapter 5aws sqs start-message-move-task --source-arn DLQ_ARN --destination-arn SRC_ARN --max-number-of-messages-per-second 5
Replay itself is causing errorsConsumer error rate rising during the taskaws sqs cancel-message-move-task --task-handle HANDLE, then lower the rate
Kafka dead-letter topic growingFailure headers (exception class, original topic, partition, offset)Fix, then republish with a small consumer, or reset offsets for an idempotent consumer
RabbitMQ DLQ growing, quorum queuex-delivery-count, the delivery-limit policyFind the failing handler; messages are dead-lettered after the limit (default 20 in 4.0 and later)
Playbook C: producers time out or get NotEnoughReplicas
Playbook C: Kafka producers time out / NotEnoughReplicas
Alert: producer errors; UnderMinIsr PartitionCount above 0 check: --describe --under-min-isr-partitions. Which brokers are missing? Cause: a broker down or out of the ISR; RF 3 with min ISR 2 and two replicas lost fix: bring the broker back (do not lower min ISR to make the error go away) check: RequestHandler AvgIdlePercent, disk and network saturation, GC pauses Cause: brokers overloaded: followers fall out of ISR, requests queue up fix: reduce load, throttle clients, add brokers, move partitions (reassignment) check: OfflinePartitionsCount, ActiveControllerCount, client logs: leader not available Cause: no leader or an unhealthy controller quorum (election in progress or stuck) fix: restore the replicas or controllers; only consider unclean election as a last resort Lowering min.insync.replicas hides the error and removes the protection it provided. Fix the replicas.
SymptomCheckCommand or fix
NotEnoughReplicasExceptionISR size versus min.insync.replicaskafka-topics.sh --describe --under-min-isr-partitions; restore the missing broker
TimeoutException after a long waitdelivery.timeout.ms (default 2 minutes) expired while retryingFind the cause above; the producer retried until the timeout and then gave up on that batch
send() blocksbuffer-available-bytes near 0; max.block.ms default 60 sBroker slow or unreachable: fix the broker; slow producers instead of growing the buffer
ISR shrink/expand flappingIsrShrinksPerSec, disk latency, GC logs on the brokersSlow disk or network on one broker: replace or move load
Leaders unbalanced after recoverykafka-topics.sh --describe leader columnkafka-leader-election.sh --bootstrap-server ... --election-type preferred --all-topic-partitions
Playbook D: messages are being processed twice
Playbook D: duplicate processing
Report: duplicate emails, double charges, repeated writes check: did the consumer exceed max.poll.interval or visibility timeout? (logs) Cause: the broker redelivered while the first attempt was still running fix: shorten the work, raise the limit sensibly, and make the handler idempotent check: producer retries without idempotence? Same business id twice in the log? Cause: a retry duplicated the send (ack lost) or the producer app sent it twice fix: enable.idempotence=true, SQS FIFO dedup id, and a business idempotency key check: was there a replay, offset reset, redrive or failover in that period? Cause: intentional re-delivery of messages already handled (at-least-once, by design) fix: dedup table on the message id (page 01 chapter 05) before any replay Duplicates are normal in at-least-once systems. The fix is rarely "stop duplicates"; it is "make them harmless".
SymptomCheckCommand or fix
Duplicates after a rebalanceCommit failures and rebalance count around the timeCommit more carefully (after processing), cooperative assignor, smaller batches
SQS: Received much greater than DeletedNumberOfMessagesReceived vs NumberOfMessagesDeletedVisibility timeout shorter than processing; increase or use ChangeMessageVisibility heartbeats
Duplicate after network error on sendProducer logs show retried send; no idempotenceKafka enable.idempotence=true; SQS FIFO deduplication id (5-minute window)
Duplicate effect, one logical messageHandler does a non-idempotent writeUpsert by business key, or insert an idempotency row in the same transaction (page 07 shows the pattern)
Write the decision, not just the fix

In your incident notes record which branch of the tree you ended in. After a few months the pattern is the most useful metric you have: if half of your lag incidents end in "slow dependency", the fix is not in the messaging layer.

11
Chapter Eleven - Interview

Questions You Should Be Able to Answer

Crisp answers on running messaging systems. Page 09 collects questions across the whole section.

  • Q: What do you monitor on a Kafka cluster?
    Under-replicated, under-min-ISR and offline partitions (data at risk), ISR shrink and expand rate, request handler and network idle percent, produce and fetch latency, disk usage, and consumer lag in offsets and in time. Page on under-min-ISR, offline partitions and old messages; the rest is for dashboards and warnings.
  • Q: Why is consumer lag in offsets not enough?
    Offsets say how much is waiting, not how long. 50,000 messages is seconds on a busy topic and a day on a quiet one. Track lag in time (age of the record being processed) and alert on that against an SLO.
  • Q: Why alert on age rather than queue depth?
    Age is the delay a user experiences and is independent of volume. Depth alone fires on healthy bursts and misses a quiet queue whose single message is hours old. On SQS use ApproximateAgeOfOldestMessage.
  • Q: What does a non-zero UnderMinIsrPartitionCount mean?
    Those partitions have fewer in-sync replicas than min.insync.replicas, so acks=all writes to them are rejected with NotEnoughReplicas. Producers are affected, and one more failure risks availability. Restore the missing replica; do not lower the setting.
  • Q: How do you restart a Kafka cluster without downtime?
    One broker at a time with controlled shutdown (controlled.shutdown.enable=true) so leadership moves first; start it, wait until under-replicated partitions are back to 0, then do the next. Replication factor 3 and min.insync.replicas=2 are what let one broker be down safely.
  • Q: How do you restart RabbitMQ nodes safely?
    Node by node, gating on rabbitmq-diagnostics check_if_node_is_quorum_critical and using rabbitmq-upgrade drain and revive, because quorum queues need a majority of replicas online. Make sure no alarms are active and feature flags are enabled before upgrading.
  • Q: How do you safely redrive a dead-letter queue?
    Inspect the messages and find the cause, fix and deploy, redrive a handful first, then the rest at a limited rate while watching consumer errors and queue age. Ensure consumers are idempotent. On SQS use the message move task with --max-number-of-messages-per-second.
  • Q: What is a poison message and how do you handle it?
    A message that always fails. Bound retries, send non-retryable errors (deserialization, validation) to the DLQ immediately, keep the context in headers, and alert on the DLQ. Without that it blocks a Kafka partition or burns retries on a queue.
  • Q: What causes a rebalance storm and how do you stop it?
    Consumers that miss max.poll.interval.ms or heartbeats get evicted, which triggers a rebalance that pauses everyone and makes the backlog worse. Reduce batch size, fix pauses, use static membership and the cooperative assignor, avoid flapping autoscaling, and consider the KIP-848 protocol.
  • Q: What happens to RabbitMQ producers when memory or disk is low?
    A resource alarm makes the node stop reading from publishing connections, so producers block, while consumers keep running. Alert on the alarm and on queue growth long before it.
  • Q: How do you design for multi-region?
    Decide RPO and RTO first; cross-region copying is asynchronous, so RPO is above zero. For Kafka use MirrorMaker 2 with an active/passive or active/active flow; for SQS run queues per Region and publish through SNS; keep consumers idempotent and rehearse the failover.
  • Q: What are your options when consumers cannot keep up?
    Buffer (let the backlog grow and scale out), slow the producer (bounded buffers, rate limits), or shed load (drop stale or low-value work). Choose per workload in advance, and set a maximum useful age for time-sensitive messages.
12
Chapter Twelve - Summary

Cheat Sheet and Recap

Cheat sheet
QuestionSignalAlert / action
Is anything late?Message age (SQS ApproximateAgeOfOldestMessage), Kafka time-lagPage when above SLO for several minutes
Is data at risk? (Kafka)UnderMinIsrPartitionCount, OfflinePartitionsCount, URPPage on the first two; URP above 0 beyond restart time is a warning
Is the cluster healthy?Request handler idle, network idle (ideally above 0.3), produce/fetch latency, diskWarn; capacity loop
Are consumers stable?rebalance-rate-per-hour, last-poll-seconds-ago against max.poll.interval.msWarn; chapter 6 fixes
RabbitMQ blocked?mem_alarm, disk_free_alarmPage: publishers are blocked
Failed messages?DLQ visible messages above 0; dead-letter rateTicket; redrive only with the checklist
Safe to restart the next node?URP 0 (Kafka); check_if_node_is_quorum_critical (RabbitMQ)Gate every step of a rolling restart
Redrive (SQS)start-message-move-task, list-message-move-tasks, cancel-message-move-taskStart at a low rate; max 500/s; one task per queue
Replay (Kafka)kafka-consumer-groups.sh --reset-offsetsGroup stopped; idempotent consumers only
Load testkafka-producer-perf-test.sh --topic --num-records --record-size --throughputProduction-like settings, kill a broker during the run
βˆ‘ Reliability and operations - what to remember
  • Monitor the path of a message, not just servers. Four questions: can producers write, is it safe, is it consumed, is the platform healthy.
  • Alert on symptoms (age, under-min-ISR, offline, alarms), investigate with causes. Every alert has a first action.
  • Age beats depth. Depth is volume; age is harm. Track Kafka lag in time as well as offsets.
  • A DLQ is a parking lot for incidents. Inspect, fix, test with a few messages, redrive with a rate limit, and be idempotent.
  • One node at a time, gated by a command: URP 0 on Kafka, quorum not critical on RabbitMQ.
  • Rebalance storms feed themselves. Shrink batches, fix pauses, static membership, cooperative assignor.
  • Choose how you fail: buffer, slow the producer, or shed load. Do it before the incident.
  • Secure by default: TLS, authentication, least-privilege ACLs and policies, encrypted at rest, no secrets in messages.
  • Test the failure modes you fear: kill consumers and brokers, send poison messages, replay, and practise the failover.
Version note

Verified on 2026-10-31 against: the Apache Kafka 4.0 documentation (monitoring MBean names and normal values, basic operations: controlled shutdown, preferred election, consumer-groups and reset-offsets options; geo-replication with MirrorMaker 2; ACL and SASL/SSL pages; the 4.0 upgrade notes), the Kafka 4.0 generated producer and consumer configuration pages (defaults quoted), and the 4.0 source of ProducerPerformance and TopicCommand for the tool options; the RabbitMQ documentation (monitoring, alarms, consumers, quorum queues, upgrade, access control, TLS) for 4.x; and the AWS documentation for SQS CloudWatch metrics, dead-letter queues, DLQ redrive (StartMessageMoveTask) and server-side encryption, SNS cross-Region delivery, and the AWS CLI help for start-message-move-task and put-metric-alarm. Not verified: exact Prometheus metric names exposed by the RabbitMQ plugin and by Kafka exporters (they vary by exporter, so this page uses MBean and CloudWatch names); alert thresholds and capacity numbers are examples to tune, not recommendations from any vendor.

Next in this section