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.
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.
Every useful signal answers one of four questions. If a metric on your dashboard answers none of them, it is decoration.
| Question | What a bad answer means | Kafka | RabbitMQ | SQS |
|---|---|---|---|---|
| Can producers write? | Data is being refused or delayed at the source | Producer record-error-rate, record-retry-rate; broker UnderMinIsrPartitionCount | Publisher connections blocked by an alarm; publish confirm latency | NumberOfMessagesSent drops while the app still runs; API throttling errors in the app |
| Is the data safe? | One more failure loses acknowledged messages | UnderReplicatedPartitions, OfflinePartitionsCount, ISR shrink and expand rate | Quorum 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 delay | Consumer lag (offsets and time) | messages_ready and consumer capacity | ApproximateAgeOfOldestMessage, ApproximateNumberOfMessagesVisible |
| Is the platform healthy? | You are about to lose the first three answers | Request handler idle, disk usage, produce and fetch latency | Memory alarm, free-disk alarm | Throttling, KMS or IAM errors in consumers |
- 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.
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.
| Signal | MBean or tool | Normal | If it is off, look at |
|---|---|---|---|
| Under-replicated partitions | kafka.server:type=ReplicaManager,name=UnderReplicatedPartitions | 0 | A broker down or slow, a saturated network or disk, a follower that cannot keep up |
| Under min-ISR partitions | ...ReplicaManager,name=UnderMinIsrPartitionCount | 0 | Writes with acks=all to these partitions are being rejected. This is the pager metric for producers |
| At min-ISR partitions | ...ReplicaManager,name=AtMinIsrPartitionCount | 0 | One more replica failure and you become under-min-ISR: an early warning |
| Offline partitions | kafka.controller:type=KafkaController,name=OfflinePartitionsCount | 0 | No leader at all: partition unavailable for reads and writes. Page immediately |
| Active controller | ...KafkaController,name=ActiveControllerCount | 1 across the cluster | 0 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 IsrExpandsPerSec | 0 except around broker restarts | Repeated shrink/expand pairs with no restart mean flapping followers: slow disks, GC pauses, network trouble |
| Unclean elections | kafka.controller:type=ControllerStats,name=UncleanLeaderElectionsPerSec | 0 | Data was lost to keep a partition available (only possible if you enabled it) |
| Request handler idle | kafka.server:type=KafkaRequestHandlerPool,name=RequestHandlerAvgIdlePercent | Ideally above 0.3 | Broker threads are saturated: too many partitions, heavy compression, slow disk, or too many clients |
| Network processor idle | kafka.network:type=SocketServer,name=NetworkProcessorAvgIdlePercent | Ideally above 0.3 | Network threads busy: many connections, large messages, TLS cost |
| Produce / fetch latency | kafka.network:type=RequestMetrics,name=TotalTimeMs,request=Produce (also FetchConsumer, FetchFollower) | Your baseline | The total is broken into queue, local, remote and response-send time. A big remote part on Produce means waiting for followers (acks=all) |
| Follower lag | kafka.server:type=ReplicaFetcherManager,name=MaxLag,clientId=Replica | Proportional to the maximum produce batch size | Followers behind the leader: precedes ISR shrinks |
| Disk usage | OS metrics per log directory; kafka-log-dirs.sh --describe | Below your alert threshold with room for retention | Retention too long, a hot partition, a stuck cleaner. A full disk takes a broker down (chapter 4) |
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.
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.
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).
- Producer:
record-error-rateandrecord-retry-rate(are sends failing or being retried),request-latency-avg, andbuffer-available-bytes(when the buffer is exhausted,send()blocks up tomax.block.ms, default 60 s, which looks like a hung request thread). - Consumer:
records-lag-max,last-poll-seconds-agocompared tomax.poll.interval.ms(default 5 minutes: a consumer that does not poll in time is removed from the group), and in the coordinator metricsrebalance-rate-per-hourandfailed-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).
UnderReplicatedPartitionsis 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 onUnderMinIsrPartitionCountandOfflinePartitionsCount.
The Same Questions on Other Brokers
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):
| Signal | Name | What it tells you |
|---|---|---|
| Ready messages | messages_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 messages | messages_unacknowledged | Delivered 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 capacity | Shown 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 alarm | mem_alarm per node | Memory is above the watermark (vm_memory_high_watermark, default 0.6 of RAM). Publishing connections are blocked |
| Free-disk alarm | disk_free_alarm; limit disk_free_limit | Free 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.
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).
SQS publishes CloudWatch metrics in the AWS/SQS namespace with a single dimension, QueueName. These are the ones that matter:
| Metric | Meaning | Use it for |
|---|---|---|
ApproximateAgeOfOldestMessage | Age in seconds of the oldest unprocessed message; reported only if the queue has at least one active message | The primary alarm. It is the delay a user is experiencing, whatever the volume |
ApproximateNumberOfMessagesVisible | Messages available for retrieval right now | Backlog size, autoscaling input, DLQ depth (any value above 0 on a DLQ) |
ApproximateNumberOfMessagesNotVisible | In-flight: received but not yet deleted or expired | Stuck or slow consumers: a growing in-flight count with no deletes. In-flight messages count against a quota (see the SQS quotas page) |
ApproximateNumberOfMessagesDelayed | Messages held by a delay queue or per-message delay | Confirming that "missing" messages are merely delayed |
NumberOfMessagesSent, NumberOfMessagesReceived, NumberOfMessagesDeleted | Throughput counters | Received much greater than deleted means messages are being redelivered (failures or a visibility timeout that is too short) |
NumberOfEmptyReceives | ReceiveMessage calls that returned nothing | Tuning polling (long polling) and API cost |
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.
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.
| Alert | Condition (example, tune to your SLO) | Severity | First action |
|---|---|---|---|
| Messages too old | SQS ApproximateAgeOfOldestMessage above your processing SLO for several consecutive minutes; Kafka time-lag above SLO | Page | Playbook A (chapter 10): is the consumer running, slow, or rebalancing? |
| DLQ not empty | DLQ visible messages above 0 (SQS), dead-letter topic receiving records, RabbitMQ DLQ messages_ready above 0 | Ticket, page if the rate is high | Playbook B: inspect before redriving |
| Data at risk | Kafka UnderMinIsrPartitionCount or OfflinePartitionsCount above 0 | Page | Playbook C |
| Replication degraded | URP above 0 for longer than a normal restart; ISR shrinks with no restart | Warning | Find the slow or missing broker |
| Broker saturated | Request handler or network idle below 0.3 for a sustained period | Warning | Capacity loop, below |
| Resource alarm | RabbitMQ memory or disk alarm active | Page (producers are blocked) | Free memory or disk, find the growing queue |
| Disk filling | Free space on a Kafka log dir or RabbitMQ data dir below a threshold, or predicted to run out within hours | Page when hours remain, not minutes | Lower retention, add storage or brokers |
| Rebalance storm | rebalance-rate-per-hour high for a group | Warning | Chapter 6 |
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.
- 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.
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.
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.
- 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.
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.
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.
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.
- 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 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.
| Step | Question | If the answer is no |
|---|---|---|
| Cause known | Do you know why they failed (code bug, bad data, dependency outage)? | Do not redrive. Keep inspecting |
| Fix deployed | Is 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 healthy | If 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 |
| Idempotent | Can every handler tolerate a message it may have partially processed before? | Add a dedup check (page 01, chapter 05) before replaying |
| Expiry safe | Are any messages stale (an expired coupon, a cancelled order)? | Discard or handle them separately: replay is not always correct |
| Rate-limited and watched | Is the replay rate bounded, and is someone watching consumer errors and age? | Start at 1 to 10 messages per second, then ramp |
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 growingx-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 1for 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).
Rebalance Storms, Backpressure and Load Shedding
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.
| Cause | What you see | Fix |
|---|---|---|
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 batch | Lower 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 blips | Fix the pauses first; do not just raise the timeout, which also delays detection of real failures |
| Rolling deploy replaces every consumer | One rebalance per restarted pod, clustered together | Use static membership (group.instance.id) so a restarted consumer rejoining within the session timeout keeps its partitions, and deploy gradually |
| Autoscaler adds and removes consumers | Rebalance rate follows the scaling graph | Add stabilisation windows; scale less often and in larger steps |
| Eager assignment moves everything | All consumers pause on every change | Use 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.
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.
| Strategy | How | Costs | Good for |
|---|---|---|---|
| Buffer (absorb) | Let the queue or log grow. Add consumers. This is why brokers exist | Latency grows; storage and retention limits become the ceiling | Bursts shorter than the time to scale up |
| Slow the producer | Bounded 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 error | When accepting work you cannot finish is worse than refusing it |
| Shed load | Drop or degrade lower-value work: sampling, dropping stale messages, skipping enrichment | Some work is lost on purpose | Metrics, 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.
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.
- 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=truethe 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-partitionsdoes 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.
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'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_criticalagainst 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_oneblocks the shutdown until enough nodes are up. - Maintenance mode:
rabbitmq-upgrade draintransfers queue primaries off the node and stops it accepting client connections;rabbitmq-upgrade revivebrings 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 quorumredistributes them.
- 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.
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 transit | TLS on listeners (SSL or SASL_SSL); broker ssl.keystore.location, ssl.truststore.location; clients ssl.truststore.location | TLS listener, conventionally port 5671: listeners.ssl.default, ssl_options.cacertfile, certfile, keyfile | HTTPS endpoints; enforce with a policy condition on aws:SecureTransport |
| Authentication | SASL 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 plugins | IAM principals and SigV4 request signing; no broker passwords |
| Authorisation | ACLs through StandardAuthorizer; kafka-acls.sh | Per-virtual-host configure / write / read permission patterns | IAM identity policies and queue / topic resource policies |
| Isolation | Prefixed ACLs and topic naming per team; quotas | Virtual hosts (rabbitmqctl add_vhost) | Separate queues and accounts; least-privilege per queue ARN |
| Encryption at rest | Disk or volume encryption (the broker does not encrypt log files itself) | Disk or volume encryption | SSE-SQS or SSE-KMS (customer-managed key) per queue |
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.
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):
- **Delete the default
guestuser** 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.
- IAM, not credentials in config. Give each service a role that allows only
sqs:ReceiveMessage,sqs:DeleteMessage,sqs:ChangeMessageVisibility(consumer) orsqs: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.comfor that topic ARN. A missing statement is the commonest reason "the subscription is confirmed but nothing arrives".
- 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.
Disaster Recovery and Testing Reliability
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.
| Platform | Pattern | Notes |
|---|---|---|
| Kafka | A 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 ways | MirrorMaker 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=2 | Survives the loss of a zone without data loss and without a second cluster. It does not survive losing the region |
| RabbitMQ | A cluster inside one region across zones with quorum queues; replicate between regions with a federation or shovel link, or at the application | Cross-region links are asynchronous. Avoid stretching one cluster over a high-latency WAN: quorum queues need low-latency links between members |
| SQS | Queues 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 standby | A 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 |
| SNS | Supports cross-Region delivery to SQS queues and Lambda functions in a different Region | A 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.
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".
| Test | How | What it proves |
|---|---|---|
| Kill a consumer mid-message | Terminate the process after it received and before it acked or committed | No loss, and the redelivered message is handled idempotently |
| Kill a broker | Stop 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 message | Publish a malformed record and an always-failing one | It reaches the DLQ with metadata and does not block the lane |
| Duplicate delivery | Send the same message twice, or force a redelivery | Exactly one business effect |
| Slow dependency | Add latency to the database the consumer uses | Backpressure, retry limits and age alarms behave as designed; no rebalance storm |
| Replay | Reset 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 test | Generate peak (and 2x peak) traffic against the real size | Capacity 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).
- Test with **the same replication factor,
acks, compression and message size as production**. A benchmark withacks=1and 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.
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.
| Symptom | Check | Command or fix |
|---|---|---|
| Lag on all partitions, group stable | Processing time and dependency health | kafka-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-hour | Lower max.poll.records, static membership, steady autoscaling |
| One partition lagging | Which key dominates it, or which offset is stuck | Look at the record at CURRENT-OFFSET; dead-letter a poison record; revisit the key |
| SQS age rising, in-flight high | ApproximateNumberOfMessagesNotVisible, deletes versus receives | Visibility timeout too short or handlers stuck: extend with ChangeMessageVisibility, fix handler |
RabbitMQ messages_unacknowledged high and flat | Consumers stuck holding deliveries; prefetch too large | rabbitmqctl list_queues name messages_ready messages_unacknowledged consumers; restart the stuck consumer; lower prefetch |
| Symptom | Check | Command or fix |
|---|---|---|
| SQS DLQ depth above 0 | Peek at messages | aws sqs receive-message --queue-url DLQ --max-number-of-messages 5 --attribute-names All --message-attribute-names All |
| Cause fixed, ready to replay | Checklist in chapter 5 | aws sqs start-message-move-task --source-arn DLQ_ARN --destination-arn SRC_ARN --max-number-of-messages-per-second 5 |
| Replay itself is causing errors | Consumer error rate rising during the task | aws sqs cancel-message-move-task --task-handle HANDLE, then lower the rate |
| Kafka dead-letter topic growing | Failure 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 queue | x-delivery-count, the delivery-limit policy | Find the failing handler; messages are dead-lettered after the limit (default 20 in 4.0 and later) |
| Symptom | Check | Command or fix |
|---|---|---|
NotEnoughReplicasException | ISR size versus min.insync.replicas | kafka-topics.sh --describe --under-min-isr-partitions; restore the missing broker |
TimeoutException after a long wait | delivery.timeout.ms (default 2 minutes) expired while retrying | Find the cause above; the producer retried until the timeout and then gave up on that batch |
send() blocks | buffer-available-bytes near 0; max.block.ms default 60 s | Broker slow or unreachable: fix the broker; slow producers instead of growing the buffer |
| ISR shrink/expand flapping | IsrShrinksPerSec, disk latency, GC logs on the brokers | Slow disk or network on one broker: replace or move load |
| Leaders unbalanced after recovery | kafka-topics.sh --describe leader column | kafka-leader-election.sh --bootstrap-server ... --election-type preferred --all-topic-partitions |
| Symptom | Check | Command or fix |
|---|---|---|
| Duplicates after a rebalance | Commit failures and rebalance count around the time | Commit more carefully (after processing), cooperative assignor, smaller batches |
| SQS: Received much greater than Deleted | NumberOfMessagesReceived vs NumberOfMessagesDeleted | Visibility timeout shorter than processing; increase or use ChangeMessageVisibility heartbeats |
| Duplicate after network error on send | Producer logs show retried send; no idempotence | Kafka enable.idempotence=true; SQS FIFO deduplication id (5-minute window) |
| Duplicate effect, one logical message | Handler does a non-idempotent write | Upsert by business key, or insert an idempotency row in the same transaction (page 07 shows the pattern) |
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.
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 useApproximateAgeOfOldestMessage. - Q: What does a non-zero UnderMinIsrPartitionCount mean?
Those partitions have fewer in-sync replicas thanmin.insync.replicas, soacks=allwrites to them are rejected withNotEnoughReplicas. 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 andmin.insync.replicas=2are what let one broker be down safely. - Q: How do you restart RabbitMQ nodes safely?
Node by node, gating onrabbitmq-diagnostics check_if_node_is_quorum_criticaland usingrabbitmq-upgrade drainandrevive, 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 missmax.poll.interval.msor 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.
Cheat Sheet and Recap
| Question | Signal | Alert / action |
|---|---|---|
| Is anything late? | Message age (SQS ApproximateAgeOfOldestMessage), Kafka time-lag | Page when above SLO for several minutes |
| Is data at risk? (Kafka) | UnderMinIsrPartitionCount, OfflinePartitionsCount, URP | Page 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, disk | Warn; capacity loop |
| Are consumers stable? | rebalance-rate-per-hour, last-poll-seconds-ago against max.poll.interval.ms | Warn; chapter 6 fixes |
| RabbitMQ blocked? | mem_alarm, disk_free_alarm | Page: publishers are blocked |
| Failed messages? | DLQ visible messages above 0; dead-letter rate | Ticket; 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-task | Start at a low rate; max 500/s; one task per queue |
| Replay (Kafka) | kafka-consumer-groups.sh --reset-offsets | Group stopped; idempotent consumers only |
| Load test | kafka-producer-perf-test.sh --topic --num-records --record-size --throughput | Production-like settings, kill a broker during the run |
- 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.
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.
- 09 Interview Q&A - questions across fundamentals, Kafka, RabbitMQ, AWS, patterns and operations, plus design prompts.
- 07 Event-Driven Patterns - outbox, saga and idempotency, the application half of reliability.
- 03 Kafka Producers & Consumers - the client settings behind the metrics in chapter 2.
- Messaging Map - the whole section.