Software Engineering WikiSE Wiki

Kafka

Operate Apache Kafka: topics, partitions and consumer groups, the shell tools, offset resets, lag, retention, compaction, producer and consumer tuning, and what to check when it misbehaves.

Reviewed MarkdownEdit

On this page

Cheatsheet#

TaskCommand
List topicskafka-topics.sh --bootstrap-server localhost:9092 --list
Create a topickafka-topics.sh --bootstrap-server localhost:9092 --create --topic my-topic --partitions 6 --replication-factor 3
Describe a topic (leaders, ISR)kafka-topics.sh --bootstrap-server localhost:9092 --describe --topic my-topic
Under-replicated partitions, cluster-widekafka-topics.sh --bootstrap-server localhost:9092 --describe --under-replicated-partitions
Add partitions (irreversible)kafka-topics.sh --bootstrap-server localhost:9092 --alter --topic my-topic --partitions 12
Topic configuration overrideskafka-configs.sh --bootstrap-server localhost:9092 --entity-type topics --entity-name my-topic --describe
Change retentionkafka-configs.sh --bootstrap-server localhost:9092 --entity-type topics --entity-name my-topic --alter --add-config retention.ms=86400000
Produce from stdinkafka-console-producer.sh --bootstrap-server localhost:9092 --topic my-topic
Consume from the startkafka-console-consumer.sh --bootstrap-server localhost:9092 --topic my-topic --from-beginning
Consume with keys and timestampskafka-console-consumer.sh --bootstrap-server localhost:9092 --topic my-topic --property print.key=true --property print.timestamp=true
List consumer groupskafka-consumer-groups.sh --bootstrap-server localhost:9092 --list
Lag per partitionkafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group my-group
Lag for every groupkafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --all-groups
Preview an offset resetkafka-consumer-groups.sh --bootstrap-server localhost:9092 --group my-group --topic my-topic --reset-offsets --to-earliest --dry-run
Latest offset of each partitionkafka-get-offsets.sh --bootstrap-server localhost:9092 --topic my-topic --time -1
Log size on disk per partitionkafka-log-dirs.sh --bootstrap-server localhost:9092 --describe --topic-list my-topic
KRaft controller quorumkafka-metadata-quorum.sh --bootstrap-server localhost:9092 describe --status
Preferred leader electionkafka-leader-election.sh --bootstrap-server localhost:9092 --election-type PREFERRED --all-topic-partitions
Delete a topic (destroys data)kafka-topics.sh --bootstrap-server localhost:9092 --delete --topic my-topic
Delete a consumer groupkafka-consumer-groups.sh --bootstrap-server localhost:9092 --delete --group my-group
Throughput testkafka-producer-perf-test.sh --topic my-topic --num-records 100000 --record-size 1024 --throughput -1 --producer-props bootstrap.servers=localhost:9092

Commands assume Kafka 4.x, which runs KRaft only; ZooKeeper support was removed in 4.0 and --zookeeper flags no longer exist. Tools live in bin/ of the distribution (/opt/kafka/bin in the official image) and every one takes --bootstrap-server; add --command-config client.properties for TLS or SASL. Reference: the Apache Kafka documentation.

Topics, partitions, offsets and groups#

A topic is a name. A partition is the unit of storage and parallelism: an append-only log of records on disk, replicated to replication-factor brokers, one of which is the leader that serves all reads and writes. An offset is a record’s position in its partition, a monotonically increasing 64-bit integer that never reuses a value even after deletion. Ordering is guaranteed within a partition only. A record with a key always lands in the same partition (murmur2 hash of the key modulo partition count), so ordering per key holds until the partition count changes.

A consumer group is a set of consumers sharing a group.id. Each partition is assigned to exactly one consumer in the group, so a group can never use more consumers than the topic has partitions; the extras sit idle. The group stores its committed offset per partition in the internal __consumer_offsets topic. Lag is log-end-offset - committed-offset, summed across partitions. Two groups reading the same topic are independent; Kafka does not delete records on consumption, only on retention.

The in-sync replica set (ISR) is the subset of a partition’s replicas that are caught up with the leader within replica.lag.time.max.ms (default 30 s). A write with acks=all is acknowledged when every ISR member has it, and min.insync.replicas sets how small the ISR may shrink before those writes are refused. A partition with fewer replicas in sync than configured is under-replicated; a partition whose leader is gone and no ISR member can take over is offline.

kafka-topics.sh --bootstrap-server localhost:9092 --describe --topic my-topic
# Topic: my-topic  PartitionCount: 6  ReplicationFactor: 3  Configs: min.insync.replicas=2,retention.ms=604800000
#   Partition: 0  Leader: 1  Replicas: 1,2,3  Isr: 1,2,3  Elr:  LastKnownElr:
#   Partition: 1  Leader: 2  Replicas: 2,3,1  Isr: 2,3    Elr:  LastKnownElr:     <- broker 1 is behind on this one

Replicas is the assignment, Isr is who is actually caught up. A leader outside Isr is impossible; a leader of -1 or none means the partition is offline.

KRaft mode#

KRaft replaces ZooKeeper with a Raft quorum of controller nodes that store cluster metadata in the internal __cluster_metadata log. Every broker replicates that log and serves metadata from a local cache. A node runs as broker, controller or both (process.roles=broker,controller, fine for development and for small clusters of three combined nodes; keep controllers separate on larger clusters so a broker under load cannot stall metadata).

# config/server.properties, combined node 1 of 3
process.roles=broker,controller
node.id=1
controller.listener.names=CONTROLLER
listeners=PLAINTEXT://:9092,CONTROLLER://:9093
advertised.listeners=PLAINTEXT://kafka-1.example.com:9092
listener.security.protocol.map=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT
inter.broker.listener.name=PLAINTEXT
controller.quorum.bootstrap.servers=kafka-1.example.com:9093,kafka-2.example.com:9093,kafka-3.example.com:9093   # dynamic quorum, Kafka 3.9+
log.dirs=/var/lib/kafka/data
num.partitions=6
default.replication.factor=3
min.insync.replicas=2
offsets.topic.replication.factor=3
transaction.state.log.replication.factor=3

Storage must be formatted with a cluster ID before the first start, and every node in a cluster must use the same ID. Formatting an already-formatted directory is refused unless --ignore-formatted is passed; passing it on a directory with data does nothing useful and mixing cluster IDs makes a broker refuse to join (INCONSISTENT_CLUSTER_ID).

KAFKA_CLUSTER_ID=$(kafka-storage.sh random-uuid)
kafka-storage.sh format --standalone -t "$KAFKA_CLUSTER_ID" -c config/server.properties        # single node: this node is the only voter
kafka-storage.sh format -t "$KAFKA_CLUSTER_ID" -c config/server.properties \
  --initial-controllers "1@kafka-1.example.com:9093:$(kafka-storage.sh random-uuid),2@kafka-2.example.com:9093:$(kafka-storage.sh random-uuid),3@kafka-3.example.com:9093:$(kafka-storage.sh random-uuid)"   # first node of a 3-controller quorum; the trailing UUID is each controller's directory.id
kafka-server-start.sh -daemon config/server.properties
kafka-metadata-quorum.sh --bootstrap-server localhost:9092 describe --status        # LeaderId, HighWatermark, CurrentVoters, CurrentObservers
kafka-metadata-quorum.sh --bootstrap-server localhost:9092 describe --replication   # per-node LogEndOffset and Lag; a voter far behind cannot vote

A quorum of n controllers tolerates (n-1)/2 failures, so run three or five, never an even number. advertised.listeners is what clients receive in metadata responses and then connect to; it is the single most common misconfiguration, and shows up as clients reaching the bootstrap server and then timing out on the address the broker advertised.

Topics and configuration#

kafka-topics.sh --bootstrap-server localhost:9092 --create --topic my-topic \
  --partitions 6 --replication-factor 3 \
  --config retention.ms=604800000 --config min.insync.replicas=2        # 7 days; acks=all needs 2 in-sync copies
kafka-topics.sh --bootstrap-server localhost:9092 --create --topic my-topic --if-not-exists ...   # idempotent in scripts
kafka-topics.sh --bootstrap-server localhost:9092 --describe --topics-with-overrides                # only topics with non-default configs
kafka-topics.sh --bootstrap-server localhost:9092 --alter --topic my-topic --partitions 12          # increase only; keyed records re-hash to new partitions
kafka-topics.sh --bootstrap-server localhost:9092 --delete --topic my-topic                         # needs delete.topic.enable=true (the default); removes every replica

Partition count cannot be reduced. Adding partitions changes which partition a key maps to, so consumers relying on per-key ordering see out-of-order records across the boundary; size the topic up front (partitions equal to the largest number of consumers you expect, rounded up) rather than growing it under load. Replication factor is changed with kafka-reassign-partitions.sh, not --alter.

Topic configuration is layered: a per-topic override set with kafka-configs.sh wins over the broker default (log.retention.ms, log.segment.bytes and so on in server.properties). Names differ between the two layers; the topic configs table lists both.

kafka-configs.sh --bootstrap-server localhost:9092 --entity-type topics --entity-name my-topic --describe
kafka-configs.sh --bootstrap-server localhost:9092 --entity-type topics --entity-name my-topic --alter \
  --add-config retention.ms=259200000,max.message.bytes=2097152                   # takes effect without restart
kafka-configs.sh --bootstrap-server localhost:9092 --entity-type topics --entity-name my-topic --alter --delete-config retention.ms   # back to broker default
kafka-configs.sh --bootstrap-server localhost:9092 --entity-type brokers --entity-name 1 --describe --all   # every effective broker setting and where it came from
kafka-configs.sh --bootstrap-server localhost:9092 --entity-type brokers --entity-default --alter --add-config log.retention.ms=172800000   # dynamic default for all brokers

Console producer and consumer#

# Keyed records from stdin: "order-1:{...}" becomes key order-1, value {...}
kafka-console-producer.sh --bootstrap-server localhost:9092 --topic my-topic \
  --property parse.key=true --property key.separator=: \
  --producer-property acks=all --producer-property compression.type=zstd
printf 'order-1:{"id":1}\norder-2:{"id":2}\n' | kafka-console-producer.sh --bootstrap-server localhost:9092 --topic my-topic --property parse.key=true --property key.separator=:

# Read the newest records as they arrive (default), or everything from the start
kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic my-topic --from-beginning \
  --property print.key=true --property print.timestamp=true --property print.partition=true --property print.offset=true
# One partition from a specific offset, stop after 10 records
kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic my-topic --partition 3 --offset 1200 --max-messages 10
# Join a group so offsets are committed; reruns resume where they stopped
kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic my-topic --group my-debug-group --timeout-ms 10000   # exit when idle for 10 s
# Read a compacted topic and show tombstones
kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic my-compacted --from-beginning --property print.key=true --property null.literal='<null>'

Without --group the console consumer generates a random console-consumer-NNNNN group and does not commit offsets, so it never interferes with real consumers; with --group my-group it joins that group and takes partitions away from the real members. Use a distinct debug group name.

kcat (formerly kafkacat) is the faster alternative for ad-hoc work: kcat -b localhost:9092 -L lists metadata, kcat -b localhost:9092 -t my-topic -C -o -10 -e prints the last 10 records per partition and exits, kcat -b localhost:9092 -t my-topic -P -K: produces keyed records.

Consumer groups, lag and offset resets#

kafka-consumer-groups.sh --bootstrap-server localhost:9092 --list --state Stable          # filter: Stable, Empty, Dead, PreparingRebalance, CompletingRebalance
kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group my-group
# GROUP     TOPIC     PARTITION  CURRENT-OFFSET  LOG-END-OFFSET  LAG   CONSUMER-ID                 HOST         CLIENT-ID
# my-group  my-topic  0          48210           48213           3     my-app-7f2c...-0d9a         /10.0.4.12   my-app
# my-group  my-topic  1          51002           59870           8868  -                           -            -          <- unassigned: no live member owns it
kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group my-group --members --verbose   # who owns what
kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group my-group --state              # coordinator broker, protocol, state, member count

A CONSUMER-ID of - with a non-zero lag means nobody is consuming that partition: the group is Empty (everything is -) or has fewer members than partitions in a way the assignor left uncovered. CURRENT-OFFSET of - means the group has never committed for that partition.

Committed offsets for an Empty group expire after offsets.retention.minutes (default 7 days). A consumer that restarts after that finds no offset and falls back to auto.offset.reset, which is latest by default, so a week-long outage silently skips everything that arrived in between. Set auto.offset.reset=earliest on consumers where reprocessing is cheaper than loss.

Offset resets rewrite the committed position and only work while the group has no active members; the tool refuses otherwise. Every reset needs --dry-run (print the new offsets) or --execute (apply them). Scope with --topic my-topic, --topic my-topic:0,1 for specific partitions, or --all-topics.

G="kafka-consumer-groups.sh --bootstrap-server localhost:9092 --group my-group --reset-offsets"
$G --topic my-topic --to-earliest --dry-run                          # reprocess everything retained
$G --topic my-topic --to-latest --execute                            # skip the backlog; the skipped records are not consumed
$G --topic my-topic --to-datetime 2026-09-24T02:00:00.000 --execute  # first offset with timestamp >= that time (broker local time, no zone suffix)
$G --topic my-topic --by-duration PT2H --execute                     # two hours before now
$G --topic my-topic:3 --shift-by -500 --execute                      # replay the last 500 on partition 3
$G --topic my-topic:3 --to-offset 48000 --execute
$G --all-topics --to-current --dry-run --export > offsets.csv        # save current positions as topic,partition,offset
$G --all-topics --from-file offsets.csv --execute                    # restore them
kafka-consumer-groups.sh --bootstrap-server localhost:9092 --delete-offsets --group my-group --topic old-topic   # forget one topic; next start uses auto.offset.reset

Resets move the pointer, not the data

--to-latest abandons unconsumed records and --to-earliest re-delivers records the application has already processed. Confirm the consumer is idempotent, or filter by timestamp downstream, before executing either. Export the current offsets first so the reset can be undone.

For continuous monitoring, kafka_exporter or the broker JMX exporter feeds Prometheus; alert on lag that grows for longer than the consumer’s expected catch-up time, not on any lag at all.

# Lag per group, summed over partitions
sum by (consumergroup, topic) (kafka_consumergroup_lag)
# Lag growing for 15 minutes: consumers cannot keep up or are stuck
sum by (consumergroup) (kafka_consumergroup_lag) > 1000 and deriv(sum by (consumergroup) (kafka_consumergroup_lag)[15m:1m]) > 0

Retention and compaction#

Each partition is a sequence of segment files. A segment rolls when it reaches segment.bytes (1 GiB) or segment.ms (7 days), and retention only ever deletes whole closed segments, never the active one. That is why a low-volume topic with retention.ms=3600000 still holds a week of data: the active segment has not rolled. Lower segment.ms (or segment.bytes) alongside retention.ms on such topics.

ConfigDefaultEffect
cleanup.policydeletedelete by age or size, compact keep the latest value per key, compact,delete both
retention.ms604800000 (7 d)Delete segments whose newest record is older; -1 keeps forever
retention.bytes-1Per-partition size cap, so multiply by partition count for the topic total
segment.bytes1073741824Roll the active segment at this size
segment.ms604800000Roll the active segment after this time even if small
min.cleanable.dirty.ratio0.5Compaction starts when this fraction of the log is uncompacted
min.compaction.lag.ms0Records younger than this are never compacted, guaranteeing readers see every update for that long
delete.retention.ms86400000 (24 h)How long a tombstone (null value) stays visible after compaction before removal
max.message.bytes1048588Largest record batch the broker accepts for this topic

Compaction keeps at least the last record for each key and removes older ones in closed segments; the head of the log is untouched, so a consumer reading from the start sees the compacted tail followed by every recent update. A record with a null value is a tombstone: it deletes the key once compaction runs and is itself dropped after delete.retention.ms, so a consumer that starts more than a day after the delete never learns the key existed. Compacted topics need keys on every record; a null key makes the producer fail for compact topics.

kafka-configs.sh --bootstrap-server localhost:9092 --entity-type topics --entity-name user-state --alter \
  --add-config cleanup.policy=compact,min.compaction.lag.ms=600000,segment.ms=3600000
kafka-log-dirs.sh --bootstrap-server localhost:9092 --describe --topic-list my-topic | tail -n +3 | jq '.brokers[].logDirs[].partitions[] | {partition, size}'   # bytes per partition per broker

Retention runs on the log.retention.check.interval.ms (5 min) schedule, so a change takes a few minutes to free space. Reducing retention is the fastest way to recover a full disk, and it is destructive in the same way a reset to latest is.

Producer settings#

SettingRecommendedWhy
acksallLeader waits for every in-sync replica; with min.insync.replicas=2 a single broker loss cannot lose acknowledged writes. 1 (leader only) and 0 trade durability for latency
enable.idempotencetrue (default since 3.0)Broker de-duplicates retried batches by producer ID and sequence, so retries never duplicate or reorder within a partition. Forces acks=all, retries>0 and max.in.flight.requests.per.connection<=5
delivery.timeout.ms120000Total budget for send plus retries; a batch that cannot be delivered in this time fails the callback. Replaces tuning retries directly
linger.ms / batch.size5–50 ms / 64–256 KiBWait to fill a batch; throughput rises sharply, latency by linger.ms at most
compression.typezstd or lz4Compresses per batch; set on the producer so the broker stores compressed bytes as-is (compression.type=producer on the topic)
max.request.size1048576Largest single request; raise together with the topic’s max.message.bytes and the broker’s replica.fetch.max.bytes
transactional.idset for exactly-onceEnables beginTransaction/commitTransaction across partitions; consumers see them only with isolation.level=read_committed

Idempotence covers one producer session and one partition. Exactly-once across a consume-transform-produce loop needs transactions: the producer commits the consumer’s offsets inside the same transaction (sendOffsetsToTransaction) so a crash either replays or commits both.

NotEnoughReplicasException on send means the ISR has shrunk below min.insync.replicas: a broker is down or lagging, and acks=all writes are refused until it recovers. That is the intended trade; lowering min.insync.replicas to keep writes flowing is choosing availability over durability.

Consumer rebalancing#

Members of a group send heartbeats to the group coordinator (the broker that leads the __consumer_offsets partition for that group). A member that misses session.timeout.ms (45 s default) or does not call poll() within max.poll.interval.ms (5 min) is removed and the group rebalances: partitions are reassigned and, under the classic protocol with the default eager assignor, every member stops consuming until the new assignment is agreed.

SettingDefaultNote
session.timeout.ms45000Missed heartbeats for this long drop the member; heartbeats run on a background thread
heartbeat.interval.ms3000Keep at a third of the session timeout
max.poll.interval.ms300000Longest gap between poll() calls; processing slower than this triggers a rebalance and CommitFailedException
max.poll.records500Records per poll(); lower it if processing a batch approaches max.poll.interval.ms
partition.assignment.strategyRangeAssignor, CooperativeStickyAssignorSet CooperativeStickyAssignor alone to only revoke partitions that actually move (incremental cooperative rebalancing)
group.instance.idunsetStatic membership: a restarted member with the same ID reclaims its partitions without a rebalance, as long as it returns within session.timeout.ms
group.protocolclassicconsumer opts in to the KIP-848 protocol (GA in 4.0): assignment is computed on the broker, rebalances are incremental and per-member, and the client-side assignor settings above no longer apply

A rebalance storm is a group that never settles: a member joins, assignment starts, another member times out, assignment restarts. Causes in order of frequency: processing exceeding max.poll.interval.ms, deployments that restart members one after another without static membership, and a consumer count that keeps changing because of an autoscaler reacting to the lag the rebalances themselves cause.

kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group my-group --state     # PreparingRebalance or CompletingRebalance for more than a few seconds is the symptom
grep -E 'Rebalance|revoked|assigned|LeaveGroup' /var/log/my-app/app.log | tail -50               # client side: who left and why

Schema registry#

Kafka stores bytes; a schema registry gives producers and consumers an agreed shape. Confluent Schema Registry (Community licence) and Apicurio Registry both expose a REST API on port 8081 and both use the same wire format: a magic byte 0, a 4-byte big-endian schema ID, then the Avro, Protobuf or JSON Schema payload. A consumer that reads such a record with a plain string deserialiser sees a leading NUL and garbage; that is the schema header, not corruption.

Schemas are versioned under a subject, by default <topic>-key and <topic>-value. A compatibility level per subject decides which changes the registry accepts: BACKWARD (default; a consumer on the new schema can read old data, so you may delete fields and add fields with defaults), FORWARD, FULL, their _TRANSITIVE variants that check every previous version, or NONE.

R=http://schema-registry.example.com:8081
curl -fsS "$R/subjects" | jq .                                              # every subject
curl -fsS "$R/subjects/my-topic-value/versions" | jq .                      # version numbers
curl -fsS "$R/subjects/my-topic-value/versions/latest" | jq -r .schema | jq . # the schema itself
curl -fsS "$R/schemas/ids/42" | jq -r .schema                               # resolve an ID seen in a record header
curl -fsS "$R/config/my-topic-value" || curl -fsS "$R/config"               # subject level, then global compatibility
# Register a new version; returns {"id": N}. Fails with 409 if incompatible with the subject's level
curl -fsS -X POST -H 'Content-Type: application/vnd.schemaregistry.v1+json' \
  --data "$(jq -n --arg s "$(cat order.avsc)" '{schemaType: "AVRO", schema: $s}')" "$R/subjects/my-topic-value/versions"
# Test compatibility without registering
curl -fsS -X POST -H 'Content-Type: application/vnd.schemaregistry.v1+json' \
  --data "$(jq -n --arg s "$(cat order.avsc)" '{schema: $s}')" "$R/compatibility/subjects/my-topic-value/versions/latest" | jq .is_compatible
curl -fsS -X PUT -H 'Content-Type: application/vnd.schemaregistry.v1+json' --data '{"compatibility":"FULL_TRANSITIVE"}' "$R/config/my-topic-value"

The registry keeps its state in the _schemas compacted topic on the Kafka cluster it points at. Deleting that topic deletes every schema, and a soft-deleted subject (DELETE /subjects/x) still occupies its IDs until a hard delete (?permanent=true).

Running locally with a container#

The official apache/kafka image starts a single combined broker-and-controller node with no configuration:

podman run -d --name kafka -p 9092:9092 apache/kafka:4.1.0
podman exec kafka /opt/kafka/bin/kafka-topics.sh --bootstrap-server localhost:9092 --create --topic my-topic --partitions 3 --replication-factor 1

The default advertises localhost:9092, so it works from the host and from podman exec and from nothing else. For other containers or another machine, set the listeners explicitly; the image maps every KAFKA_* environment variable to the matching server.properties key (upper case, dots as underscores):

services:
  kafka:
    image: apache/kafka:4.1.0
    ports: ["9092:9092"]
    environment:
      KAFKA_NODE_ID: 1
      KAFKA_PROCESS_ROLES: broker,controller
      KAFKA_LISTENERS: PLAINTEXT://:9092,CONTROLLER://:9093,INTERNAL://:19092
      KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://localhost:9092,INTERNAL://kafka:19092   # host clients use localhost, compose peers use kafka:19092
      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT,INTERNAL:PLAINTEXT
      KAFKA_INTER_BROKER_LISTENER_NAME: INTERNAL
      KAFKA_CONTROLLER_LISTENER_NAMES: CONTROLLER
      KAFKA_CONTROLLER_QUORUM_VOTERS: 1@localhost:9093
      KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1          # defaults of 3 make the internal topics uncreatable on one broker
      KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
      KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
      KAFKA_LOG_DIRS: /var/lib/kafka/data
    volumes:
      - kafka-data:/var/lib/kafka/data:Z
volumes:
  kafka-data: {}

See Docker Compose for the stack conventions. A single-node cluster cannot satisfy replication-factor 3 or min.insync.replicas=2; create topics with --replication-factor 1 locally and keep the production values in the deployment manifests, not in application code.

Oneliners#

# Total lag per group, sorted
kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --all-groups 2>/dev/null | awk 'NR>1 && $6 ~ /^[0-9]+$/ {lag[$1]+=$6} END {for (g in lag) print lag[g], g}' | sort -rn

# Records per partition (latest minus earliest offset)
paste <(kafka-get-offsets.sh --bootstrap-server localhost:9092 --topic my-topic --time -2) <(kafka-get-offsets.sh --bootstrap-server localhost:9092 --topic my-topic --time -1) | awk -F'[:\t]' '{print $2, $6-$3}'

# Partitions with no leader (unavailable)
kafka-topics.sh --bootstrap-server localhost:9092 --describe --unavailable-partitions

# Partitions whose leader is not the preferred (first) replica
kafka-topics.sh --bootstrap-server localhost:9092 --describe | awk '/Partition:/ {split($8,r,","); if ($6 != r[1]) print}'

# Topics with replication factor below 3
kafka-topics.sh --bootstrap-server localhost:9092 --describe | awk '/ReplicationFactor:/ && $8 < 3 {print $2, "rf=" $8}'

# Every topic's retention override
for t in $(kafka-topics.sh --bootstrap-server localhost:9092 --list); do printf '%s\t' "$t"; kafka-configs.sh --bootstrap-server localhost:9092 --entity-type topics --entity-name "$t" --describe | grep -o 'retention.ms=[0-9-]*' || echo default; done

# Disk used per broker, all topics
kafka-log-dirs.sh --bootstrap-server localhost:9092 --describe | tail -n +3 | jq -r '.brokers[] | "\(.broker) \([.logDirs[].partitions[].size] | add / 1e9 | floor) GB"'

# Tail a topic and pretty-print JSON values
kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic my-topic | jq -c .

# Last record of each partition
kcat -b localhost:9092 -t my-topic -C -o -1 -e -f '%p:%o %k %s\n'

# Timestamp of the oldest retained record on partition 0
kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic my-topic --partition 0 --offset earliest --max-messages 1 --property print.timestamp=true --property print.value=false

# Which broker coordinates a group
kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group my-group --state | awk 'NR==2 {print $2}'

# Members of a group and their partition counts
kafka-consumer-groups.sh --bootstrap-server localhost:9092 --describe --group my-group --members

# Groups with no members (offsets will expire after offsets.retention.minutes)
kafka-consumer-groups.sh --bootstrap-server localhost:9092 --list --state Empty

# Inspect a segment file on a broker (record batches, keys, values)
kafka-dump-log.sh --files /var/lib/kafka/data/my-topic-0/00000000000000000000.log --print-data-log | head -40

# Producer throughput test with realistic settings
kafka-producer-perf-test.sh --topic perf-test --num-records 500000 --record-size 512 --throughput -1 --producer-props bootstrap.servers=localhost:9092 acks=all linger.ms=10 compression.type=zstd

# Consumer throughput test
kafka-consumer-perf-test.sh --bootstrap-server localhost:9092 --topic perf-test --messages 500000 --group perf-test-group

# Move every partition off broker 3 before decommissioning it (generate, then execute the JSON it prints)
kafka-reassign-partitions.sh --bootstrap-server localhost:9092 --topics-to-move-json-file topics.json --broker-list 1,2,4 --generate

# Progress of a running reassignment
kafka-reassign-partitions.sh --bootstrap-server localhost:9092 --reassignment-json-file reassign.json --verify

# Broker API versions: confirms connectivity and the broker's version through any listener
kafka-broker-api-versions.sh --bootstrap-server kafka-1.example.com:9092 | head -3

# ACLs for a principal
kafka-acls.sh --bootstrap-server localhost:9092 --list --principal User:my-app --command-config admin.properties

Scripts#

Report every group whose lag exceeds a threshold, for a cron job or a Nagios-style check.

#!/usr/bin/env bash
# usage: kafka-lag-check.sh <bootstrap> <threshold>; exit 2 when any group exceeds it
set -euo pipefail
bootstrap=${1:?bootstrap server required}
threshold=${2:-10000}
rc=0
while read -r lag group; do
  [[ $lag =~ ^[0-9]+$ ]] || continue
  if (( lag > threshold )); then
    printf 'CRITICAL %s lag=%s\n' "$group" "$lag"
    rc=2
  fi
done < <(
  timeout 60s kafka-consumer-groups.sh --bootstrap-server "$bootstrap" --describe --all-groups 2>/dev/null \
    | awk 'NR>1 && $6 ~ /^[0-9]+$/ {lag[$1]+=$6} END {for (g in lag) print lag[g], g}'
)
(( rc == 0 )) && echo "OK all groups under $threshold"
exit "$rc"

Audit topic durability: flag anything with a replication factor below 3 or without min.insync.replicas=2.

#!/usr/bin/env bash
set -euo pipefail
bootstrap=${1:?bootstrap server required}
kafka-topics.sh --bootstrap-server "$bootstrap" --describe 2>/dev/null \
  | awk '/ReplicationFactor:/ && $2 !~ /^__/ {print $2, $8, $0}' \
  | while read -r topic rf rest; do
      isr=$(grep -o 'min.insync.replicas=[0-9]*' <<<"$rest" | cut -d= -f2 || true)
      if (( rf < 3 )); then
        printf '%s\trf=%s\tbelow 3\n' "$topic" "$rf"
      elif [[ ${isr:-1} -lt 2 ]]; then
        printf '%s\trf=%s\tmin.insync.replicas=%s\n' "$topic" "$rf" "${isr:-default}"
      fi
    done

Apply a retention change to every topic matching a pattern, with a dry run by default.

#!/usr/bin/env bash
# usage: kafka-bulk-retention.sh <bootstrap> <regex> <retention.ms> [apply]
set -euo pipefail
bootstrap=${1:?} pattern=${2:?} retention=${3:?} mode=${4:-dry-run}
mapfile -t topics < <(kafka-topics.sh --bootstrap-server "$bootstrap" --list | grep -E "$pattern" | grep -v '^__')
(( ${#topics[@]} )) || { echo "no topics match $pattern" >&2; exit 1; }
for t in "${topics[@]}"; do
  if [[ $mode == apply ]]; then
    kafka-configs.sh --bootstrap-server "$bootstrap" --entity-type topics --entity-name "$t" --alter --add-config "retention.ms=$retention"
  else
    printf 'would set retention.ms=%s on %s\n' "$retention" "$t"
  fi
done

Save every group’s committed offsets before a risky change, so a reset can be undone with --from-file.

#!/usr/bin/env bash
set -euo pipefail
bootstrap=${1:?} outdir=${2:-offsets-$(date +%Y%m%dT%H%M%S)}
mkdir -p "$outdir"
for g in $(kafka-consumer-groups.sh --bootstrap-server "$bootstrap" --list); do
  kafka-consumer-groups.sh --bootstrap-server "$bootstrap" --group "$g" --all-topics --reset-offsets --to-current --dry-run --export > "$outdir/$g.csv" 2>/dev/null \
    && printf 'saved %s (%s partitions)\n' "$g" "$(wc -l < "$outdir/$g.csv")"
done

Troubleshooting#

SymptomCauseFix
Lag grows steadily on every partitionConsumers slower than producers--describe --group to confirm all partitions are owned; add consumers up to the partition count, raise max.poll.records only if processing is per-batch, profile the consumer
Lag grows on one partitionHot key, or one consumer stuck--describe --group --members --verbose shows the owner; check that instance’s logs and GC; repartition the key if one key dominates
Lag high, CONSUMER-ID is -No live member, or offsets from a group that is EmptyStart the consumer; if the group has been empty longer than offsets.retention.minutes, expect auto.offset.reset to apply
Under-replicated partitionsA broker is down, slow disk or network, or replica fetchers behind after a restart--describe --under-replicated-partitions, then that broker’s server.log; a count that stays flat after a restart means a follower cannot catch up
Group stuck in PreparingRebalanceA member joined or left repeatedly; processing exceeds max.poll.interval.ms--describe --group --state; client logs for Member ... sending LeaveGroup or poll() timed out; lower max.poll.records, use static membership, enable cooperative or KIP-848 protocol
RecordTooLargeException on produceRecord exceeds producer max.request.sizeRaise it on the producer, then max.message.bytes on the topic and replica.fetch.max.bytes on brokers; consumers need fetch.max.bytes and max.partition.fetch.bytes to match
MESSAGE_TOO_LARGE from the brokerBatch exceeds topic max.message.bytesSame chain; compressed size is what counts on the broker, uncompressed on the producer
NotEnoughReplicasExceptionISR below min.insync.replicas with acks=allRestore the missing broker; do not lower min.insync.replicas to work around it
Timed out waiting for a node assignment or connects then hangsadvertised.listeners returns an address the client cannot reachkafka-broker-api-versions.sh --bootstrap-server host:9092 from the client’s network; fix advertised.listeners for that listener
CommitFailedExceptionMember was removed before the commit; usually max.poll.interval.ms exceededLower max.poll.records, raise max.poll.interval.ms, move slow work off the poll thread
OffsetOutOfRangeExceptionCommitted offset points at deleted data (retention outran the consumer)auto.offset.reset decides; reset explicitly with --to-earliest after checking retention
UNKNOWN_TOPIC_OR_PARTITIONTopic missing and auto.create.topics.enable=false (recommended), or a metadata cache lag right after creationCreate it explicitly; retries after a few seconds succeed for the cache case
INCONSISTENT_CLUSTER_ID on startmeta.properties in log.dirs belongs to another clusterWrong data directory or a reused volume; do not reformat a directory that holds production data
Broker disk full, No space left on deviceRetention too generous, or a segment that never rollskafka-log-dirs.sh --describe; lower retention.ms on the biggest topics, wait for the cleanup interval; add segment.ms on quiet topics
Controller quorum has no leaderFewer than a majority of voters reachablekafka-metadata-quorum.sh describe --replication; restore voters, never format a voter to force a new quorum

Further reading#