Kafka Operations and Monitoring

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages.

Kafka Architecture explained what a broker, controller, and partition replica are; this page is about keeping a cluster built from them healthy once it is running — the command-line tools an operator reaches for daily, what to watch on a dashboard, how to replicate data between clusters, and how a cluster actually gets deployed and run.

The command-line toolbox

Every tool below ships in Kafka’s bin/ directory (bin/kafka-.sh on Linux/macOS, bin\windows\kafka-.bat on Windows) and, since Kafka 3.0, talks to the cluster directly over the Kafka protocol via --bootstrap-server — none of them need direct access to the controller quorum’s storage.

Tool Purpose

kafka-topics.sh

Create, list, describe, alter (partition count, most topic-level configs), and delete topics.

kafka-consumer-groups.sh

List and describe consumer groups — including the per-partition lag table used below — reset committed offsets, and delete groups.

kafka-configs.sh

Describe and dynamically alter configuration for any entity type (topics, brokers, broker-loggers, users, clients, ips) without restarting anything; see "Dynamic configuration" below.

kafka-console-producer.sh / kafka-console-consumer.sh

Produce or consume from a terminal for ad hoc testing — reading stdin line by line, or printing records as they arrive — rather than writing a throwaway client.

kafka-features.sh

Describe and upgrade the cluster’s feature versions (metadata.version and others), the KIP-584 mechanism KRaft uses to gate metadata-log record formats and RPC behavior changes so every broker and controller in the quorum agrees on what it may use before a feature is turned on cluster-wide.

# create a topic with an explicit partition count and replication factor
bin/kafka-topics.sh --bootstrap-server localhost:9092 \
    --create --topic orders --partitions 12 --replication-factor 3

# describe one consumer group's progress against every partition it reads
bin/kafka-consumer-groups.sh --bootstrap-server localhost:9092 \
    --describe --group order-processors

# GROUP             TOPIC   PARTITION  CURRENT-OFFSET  LOG-END-OFFSET  LAG  CONSUMER-ID          HOST         CLIENT-ID
# order-processors   orders  0          10482           10490           8   consumer-1-6f2a...   /10.0.0.5    consumer-1
# order-processors   orders  1          9931            9931            0   consumer-2-9c1d...   /10.0.0.6    consumer-2

# roll the cluster's feature versions forward to a target Kafka release's defaults
bin/kafka-features.sh --bootstrap-server localhost:9092 \
    --release-version 4.3 upgrade

The LAG column above — log-end offset minus the group’s committed offset, per partition — is exactly what "Consumer lag" below defines, expressed as a one-off snapshot instead of a continuously scraped metric; running this command is the fastest way to check a specific group by hand.

How many partitions?

Kafka Architecture already covers what a partition is — the unit of ordering, parallelism, and per-consumer-group throughput; this is the operational question of how many a topic should actually have. Three factors usually decide it: the target aggregate throughput divided by what one partition can sustain (a few or a few dozen MB/s depending on hardware, replication factor, and compression — benchmark rather than assume); the number of consumer instances that must be able to read the topic in parallel within a single group, since a topic with P partitions can never keep more than P consumers in one group busy at once (see Kafka Consumers); and per-partition overhead on the brokers and controller — open file handles, replication traffic, and metadata records — which is what stops "just create thousands of partitions to be safe" from being free. kafka-topics.sh --alter --partitions can only increase a topic’s partition count, never reduce it, and increasing it reshuffles which partition a given key hashes to for any producer using the default partitioner — so it is worth erring generously (over-provisioning within reason) up front rather than treating the initial count as easily reversible later.

Dynamic configuration

Most topic-, broker-, and client-level settings can be changed on a live cluster with kafka-configs.sh --alter, without restarting a single process — the change is written to the metadata log and picked up by every broker that needs it. This is what makes tuning retention, throttling a noisy client, or fixing a too-low min.insync.replicas an operational action rather than a deployment.

# raise retention for a single topic
bin/kafka-configs.sh --bootstrap-server localhost:9092 \
    --entity-type topics --entity-name orders \
    --alter --add-config retention.ms=1209600000

# throttle one client's produce/consume byte rate to protect the rest of the cluster
bin/kafka-configs.sh --bootstrap-server localhost:9092 \
    --entity-type clients --entity-name reporting-service \
    --alter --add-config 'producer_byte_rate=1048576,consumer_byte_rate=2097152'

# inspect the effective (default + override) configuration for a topic
bin/kafka-configs.sh --bootstrap-server localhost:9092 \
    --entity-type topics --entity-name orders --describe

Not every setting is dynamic — some, such as a broker’s listener list or log directories, are read once at startup — but the common levers operators reach for repeatedly (retention, compaction thresholds, min.insync.replicas, per-client quotas) all are.

Key broker metrics to watch

Kafka exposes its internal state as JMX MBeans; whatever metrics pipeline scrapes them (Prometheus’s JMX exporter, Datadog, CloudWatch agent, and so on), these are the ones worth an alert rather than just a dashboard panel.

Metric (JMX MBean) What it means

kafka.server:type=ReplicaManager,name=UnderReplicatedPartitions

Count of partitions whose in-sync replica set is currently smaller than their full replica set. Zero is the healthy state; any sustained non-zero value means followers are falling behind or a broker is down — see Kafka Reliability and Exactly-Once for what a shrunk ISR does to acks=all writes once it drops below min.insync.replicas.

kafka.controller:type=KafkaController,name=ActiveControllerCount

Each broker reports 0 or 1; summed across the cluster this should be exactly 1 at all times. 0 everywhere means an election is in progress (transient); more than 1 summed across the cluster indicates a split-brain condition that should never happen in a healthy KRaft quorum. See Kafka Architecture for what the active controller does.

kafka.network:type=RequestMetrics,name=TotalTimeMs,request=Produce (and request=FetchConsumer for fetches)

End-to-end request latency, broken down by companion metrics (RequestQueueTimeMs/LocalTimeMs/RemoteTimeMs/ResponseSendTimeMs) into queue wait, local processing, time waiting on other brokers (e.g. followers for acks=all), and response transmission — the breakdown is what tells you why a request is slow rather than just that it is.

Consumer lag

Lag is a partition’s log-end offset minus a consumer group’s last committed offset on that partition — how many records the group still has to catch up on. It is the single most useful number for answering "is this consumer keeping up," independent of whether the consumer process looks healthy: a consumer can be alive, polling, and heartbeating normally while still falling further behind every minute if it can’t process records as fast as they arrive. kafka-consumer-groups.sh --describe (shown above) reports it as a point-in-time snapshot; the same figure is available continuously per consumer instance as the records-lag-max client metric (kafka.consumer:type=consumer-fetch-manager-metrics,client-id=…​), which is what an alerting pipeline should scrape rather than polling the CLI tool on a cron.

Quotas

Quotas cap how much of a broker’s resources one client is allowed to consume, so a single noisy or misbehaving client can’t starve every other client sharing the cluster. Kafka enforces three kinds, all set the same way as any other dynamic configuration via kafka-configs.sh --entity-type clients|users --alter --add-config: a produce byte-rate quota (producer_byte_rate), a consume byte-rate quota (consumer_byte_rate), and a request quota (request_percentage) that limits the fraction of a broker’s request-handling threads a client may occupy, independent of the bytes it moves. Quotas can be scoped to a client-id, a user (once authentication is in place — see Kafka Security), or the combination of both, and a default can be set at any of those levels with --entity-default so that new clients get a sane cap automatically instead of running unthrottled until someone notices.

Geo-replication with MirrorMaker 2

MirrorMaker 2 replicates topics, consumer group offsets, topic configuration, and ACLs from a source cluster to a target cluster, running as a set of connectors (MirrorSourceConnector, MirrorCheckpointConnector, MirrorHeartbeatConnector) on top of Kafka Connect's distributed-mode worker framework rather than as a bespoke replication process. The original MirrorMaker (kafka-mirror-maker.sh, sometimes called "MirrorMaker 1"), a much simpler tool that only copied records and predates this connector-based architecture, was deprecated in Kafka 3.0 under KIP-720 and removed entirely in Kafka 4.0 — MirrorMaker 2 is the only mirroring tool Kafka itself ships today.

A replication flow is configured as <source-cluster>→<target-cluster>.enabled=true, and which topics it covers is a regex allowlist (topics). Two topologies cover most needs: active/passive, a single one-way flow used for disaster recovery, where the target cluster is a standby that is promoted only on failover; and active/active, a pair of flows running in both directions so that either cluster can serve local reads and writes with the other kept in sync — typically for geographic locality (clients in each region talking to their nearest cluster). Active/active needs a way to stop a record from bouncing back and forth forever between the two flows: MirrorMaker 2’s default DefaultReplicationPolicy renames a topic as it crosses — a topic named orders on cluster us-west arrives on us-east as us-west.orders — so a flow never re-mirrors a topic that already carries the other cluster’s prefix.

flowchart LR subgraph ClusterA["Cluster us-west"] TA["Topic: orders"] TA2["Topic: us-east.orders\n(mirrored from Cluster us-east)"] end subgraph ClusterB["Cluster us-east"] TB["Topic: orders"] TB2["Topic: us-west.orders\n(mirrored from Cluster us-west)"] end MM2AB["MirrorMaker 2\nflow: us-west -> us-east"] MM2BA["MirrorMaker 2\nflow: us-east -> us-west"] TA -->|reads| MM2AB MM2AB -->|writes renamed topic| TB2 TB -->|reads| MM2BA MM2BA -->|writes renamed topic| TA2

MirrorCheckpointConnector additionally replicates consumer group offsets (translated between the two clusters' offset spaces) so a consumer failing over from one cluster to the other can resume close to where it left off, and MirrorHeartbeatConnector writes a periodic heartbeat topic used to monitor whether a flow is actually making progress. See Apache Kafka Documentation — Geo-Replication (Cross-Cluster Data Mirroring) for the full connector configuration reference, offset translation details, and fan-out/aggregation topologies beyond the two covered here.

Tiered storage operations

Kafka Architecture introduced tiered storage’s local/remote split conceptually; operating it is two separate switches. The cluster-wide remote.log.storage.system.enable flag is a broker configuration (set in server.properties, taking effect on that broker’s next restart, not a live kafka-configs.sh change) that turns the feature on for the cluster; enabling it per topic is then an ordinary dynamic change:

bin/kafka-configs.sh --bootstrap-server localhost:9092 \
    --entity-type topics --entity-name orders \
    --alter --add-config remote.storage.enable=true

# how much of the local tier a broker keeps before offloading a segment to the remote tier
bin/kafka-configs.sh --bootstrap-server localhost:9092 \
    --entity-type topics --entity-name orders \
    --alter --add-config local.retention.ms=86400000

Once enabled, reads for offsets still in the local tier are served exactly as before; reads for older offsets are fetched transparently from the remote tier through the pluggable RemoteStorageManager — a kafka-console-consumer.sh --from-beginning against a long-retention topic is a simple way to confirm both tiers are being read correctly end to end. See Apache Kafka Documentation — Tiered Storage for the full configuration reference, including how disabling tiered storage on a topic that already has remote data is handled.

Deployment options

Kafka’s own documentation walks running a broker via the official images: apache/kafka (JVM-based, published from Kafka 3.7 on) and the experimental, GraalVM-native apache/kafka-native (from 3.8 on, explicitly called out as suitable for local development and testing only, not production) — Getting Started with Messaging already uses the former for the single-node docker run quickstart; see Apache Kafka Documentation — Docker for both images and the multi-broker Docker Compose examples in Kafka’s own repository it links to.

Running and upgrading a cluster of brokers by hand — coordinating rolling restarts, certificate rotation, storage, and scaling — is a substantial operational burden most teams don’t take on directly. Strimzi is the most common answer for running Kafka on Kubernetes: an operator that manages brokers, controllers, topics, users, and MirrorMaker 2 flows as Kubernetes custom resources (Kafka, KafkaTopic, KafkaUser, KafkaMirrorMaker2), reconciling the running cluster to match them the way any Kubernetes operator does. Fully managed services — Confluent Cloud, Amazon MSK, and similar offerings from other cloud providers — go a step further and remove broker operation from the team entirely, at the cost of losing direct control over broker configuration and upgrade timing. Which of the three (self-managed, Strimzi-on-Kubernetes, fully managed) fits best is mostly a question of how much operational capacity a team already has and how much control over broker internals it actually needs — all three run the same Kafka protocol described in Protocols, so client code does not change between them.