Kafka alerting in Kubernetes: consumer lag, partitions, and disk
Published: 2026-02-18
Kafka in production needs four categories of alerting: broker availability, replication health, consumer lag, and resource saturation. Each category has different urgency and different metrics sources. Here's the PrometheusRule set we use in Kubernetes with kafka-exporter.
kafka-exporter deployment
kafka-exporter scrapes Kafka's metrics via the Kafka API (AdminClient) and exposes them in Prometheus format. It does not use JMX — which is why it's lighter than other Kafka exporters. We deploy it via the prometheus-kafka-exporter Helm chart:
yamlapiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: kafka-exporter
spec:
chart:
spec:
chart: prometheus-kafka-exporter
version: "3.0.1"
sourceRef:
kind: HelmRepository
name: prometheus-community
namespace: flux-system
values:
kafkaServer:
- kafka.kafka.svc.cluster.local:9092
prometheus:
serviceMonitor:
enabled: true
additionalLabels:
release: kube-prom-stack
interval: 30s
relabelings:
- targetLabel: job
replacement: kafka
resources:
requests:
cpu: 25m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
The relabelings block overrides the default job label to kafka. All our alert expressions use job="kafka" to target these metrics. Without this, the job label defaults to the HelmRelease name or the ServiceMonitor name, which varies across environments.
What kafka-exporter exposes
Key metrics:
kafka_brokers— number of brokers in the clusterkafka_topic_partitions— partition count per topickafka_topic_partition_leader— which broker is leader for each partitionkafka_topic_partition_under_replicated_partition— 1 if a partition is under-replicatedkafka_consumergroup_lag— lag per partition per consumer groupkafka_consumergroup_lag_sum— total lag per consumer group
Broker availability
yaml- alert: KafkaDown
expr: up{job="kafka"} < 1
for: 5m
labels:
severity: disaster
service: kafka
groupp: admin
url: 'https://grafana.example.com/d/kafka-overview/kafka-overview'
event: 'Kafka Down |{{$labels.instance}}'
annotations:
description: >-
Kafka broker {{$labels.instance}} has been down for more than 5 min.
Timestamp: {{ with query "time()+10800" }}{{ . | first | value | humanizeTimestamp }}{{ end }}
yaml- alert: KafkaExporterMissing
expr: absent(kafka_brokers{job="kafka"})
for: 5m
labels:
severity: high
service: kafka
event: 'Kafka Exporter Missing'
annotations:
description: No kafka_brokers series present — exporter may be down or can't reach Kafka.
The absent() alert catches the case where the exporter itself fails to scrape Kafka. Without it, KafkaDown would never fire because there'd be no up series at all — Prometheus would simply have no data rather than a 0 value.
Broker count monitoring
For multi-broker clusters:
yaml- alert: KafkaBrokerCountChanged
expr: kafka_brokers{job="kafka"} < 3
for: 5m
labels:
severity: high
event: 'Kafka Broker Count Low |{{$value}} brokers'
annotations:
description: Expected 3 Kafka brokers, only {{$value}} are visible. A broker may be down.
This catches the case where a broker fails to register with ZooKeeper/KRaft but the up metric for the exporter is still healthy (the exporter is running but can't see all brokers).
Under-replicated partitions
yaml- alert: KafkaUnderReplicatedPartitions
expr: sum(kafka_topic_partition_under_replicated_partition{job="kafka"}) > 0
for: 10m
labels:
severity: high
service: kafka
event: 'Kafka Under-Replicated Partitions'
annotations:
description: >-
Kafka has {{$value}} under-replicated partitions for more than 10 min.
A replica is lagging behind or a broker is slow. This is a leading indicator of data loss risk.
Under-replicated partitions are a leading indicator: they appear before a broker actually goes down. If a broker fails while partitions are under-replicated, some messages have only one copy — no redundancy.
Why 10m and not 5m? Brief under-replication happens during rolling updates and leader elections. A 10-minute for filters out normal operational noise.
Offline partitions
More urgent than under-replicated:
yaml- alert: KafkaOfflinePartitions
expr: sum(kafka_topic_partition_leader{job="kafka"} < 0) > 0
for: 1m
labels:
severity: disaster
event: 'Kafka Offline Partitions |{{$value}} partitions'
annotations:
description: >-
{{$value}} Kafka partitions have no leader. Producers and consumers for these
partitions will fail immediately.
A leader < 0 value indicates no leader is elected for that partition. Producers and consumers for those partitions fail with LEADER_NOT_AVAILABLE.
Consumer lag
yaml- alert: KafkaConsumerLagCritical
expr: >
sum(kafka_consumergroup_lag_sum{job="kafka"}) by (consumergroup) > 100000
for: 5m
labels:
severity: average
service: kafka
event: 'Kafka Consumer Lag |{{$labels.consumergroup}}'
annotations:
description: >-
Consumer group {{$labels.consumergroup}} lag is {{$value}} messages
(above threshold of 100 000) for more than 5 min.
The threshold 100000 is workload-specific. Set it based on your consumer throughput:
- Consumer processes 10k messages/sec → lag of 100k = 10 seconds behind (acceptable)
- Consumer processes 100/sec → same lag = 1000 seconds (critical)
Tune thresholds per consumer group with separate rules:
yaml- alert: KafkaHighPriorityConsumerLag
expr: >
kafka_consumergroup_lag_sum{job="kafka",
consumergroup="payment-processor"} > 500
for: 2m
labels:
severity: high
event: 'Payment Consumer Lag |{{$labels.consumergroup}}'
Consumer group not consuming
An alert for consumer groups that have stopped consuming entirely:
yaml- alert: KafkaConsumerGroupStopped
expr: >
delta(kafka_consumergroup_lag_sum{job="kafka"}[5m]) > 1000
and
kafka_consumergroup_lag_sum{job="kafka"} > 1000
for: 5m
labels:
severity: high
event: 'Kafka Consumer Stopped |{{$labels.consumergroup}}'
annotations:
description: >-
Consumer group {{$labels.consumergroup}} lag has grown by more than 1000
messages in 5 minutes and is not catching up.
This catches lag that's growing (not just high) — meaning the consumer is alive but can't keep up with production rate.
Disk usage (Kafka PVCs)
kafka-exporter doesn't expose disk metrics — those come from kubelet:
yaml- alert: KafkaHighDiskUsage
expr: >
sum(kubelet_volume_stats_used_bytes{namespace="kafka"})
by (namespace, persistentvolumeclaim)
/
sum(kubelet_volume_stats_capacity_bytes{namespace="kafka"})
by (namespace, persistentvolumeclaim)
> 0.9
for: 15m
labels:
severity: warning
service: kafka
event: 'Kafka High Disk |{{$labels.persistentvolumeclaim}}'
annotations:
description: >-
Kafka PVC {{$labels.persistentvolumeclaim}} is above 90% full.
Consider reducing retention or adding storage.
Kafka has configurable retention (log.retention.hours, log.retention.bytes). If disk is filling up, the quickest fix is to reduce log.retention.hours on the affected topics:
bashkafka-configs.sh --bootstrap-server kafka:9092 \
--entity-type topics \
--entity-name my-topic \
--alter --add-config retention.ms=3600000 # 1 hour
Memory usage
yaml- alert: KafkaHighMemoryUsage
expr: process_resident_memory_bytes{job="kafka"} / 1024 / 1024 / 1024 > 4
for: 15m
labels:
severity: warning
event: 'Kafka High Memory |{{$labels.instance}}'
annotations:
description: Kafka broker {{$labels.instance}} is using more than 4 GB RSS for 15 min.
Kafka brokers are JVM processes. RSS growing above the heap limit usually means page cache usage — not necessarily a problem. But crossing the container memory limit causes OOM kills. This alert gives time to adjust KAFKA_HEAP_OPTS or resources.limits.memory.
For Kubernetes Kafka (Strimzi or Bitnami chart), set JVM heap explicitly:
yamlvalues:
extraEnvVars:
- name: KAFKA_HEAP_OPTS
value: "-Xmx2g -Xms2g"
resources:
limits:
memory: 4Gi # heap (2g) + off-heap + overhead
Complete PrometheusRule manifest
yamlapiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: kafka-alerts
namespace: observability
labels:
release: kube-prom-stack
spec:
groups:
- name: kafka
interval: 30s
rules:
# Broker availability
- alert: KafkaDown
expr: up{job="kafka"} < 1
for: 5m
labels: { severity: disaster, service: kafka }
- alert: KafkaExporterMissing
expr: absent(kafka_brokers{job="kafka"})
for: 5m
labels: { severity: high, service: kafka }
# Replication health
- alert: KafkaUnderReplicatedPartitions
expr: sum(kafka_topic_partition_under_replicated_partition{job="kafka"}) > 0
for: 10m
labels: { severity: high, service: kafka }
# Consumer lag
- alert: KafkaConsumerLagCritical
expr: sum(kafka_consumergroup_lag_sum{job="kafka"}) by (consumergroup) > 100000
for: 5m
labels: { severity: average, service: kafka }
# Disk
- alert: KafkaHighDiskUsage
expr: >
sum(kubelet_volume_stats_used_bytes{namespace="kafka"}) by (persistentvolumeclaim)
/ sum(kubelet_volume_stats_capacity_bytes{namespace="kafka"}) by (persistentvolumeclaim)
> 0.9
for: 15m
labels: { severity: warning, service: kafka }