Free lesson · GenAI Platform Engineering
Monitor Kafka with consumer lag metrics
You alarm on consumer lag (rows + time), under-replicated partitions, and rebalance storms via kafka-exporter and Prometheus.
Course: Data Infrastructure Essentials for GenAI · Chapter 5 · Kafka Fundamentals
Free to read — no subscription required.
Introduction
When you ship AI inference or embedding pipelines on Kafka, lag is the metric that decides whether users see fresh answers or stale ones — and it is the one most often misinterpreted. Lag is the per-partition difference between producer-written offsets and consumer-committed offsets — how far behind real-time your pipeline is. Get it wrong and a 4,000-record backlog on inference-requests becomes a 20-second user-visible latency spike that no APM dashboard surfaces, while RAG quietly returns stale context because embeddings-ingest fell minutes behind. By the end you will be able to measure lag accurately, distinguish absolute lag from lag-time, alert on what users feel, and run the slow-consumer response runbook (scale up, rewind, replay).
Key Terminology
- Log-end-offset (LEO) — the next offset producers will write to a partition; the moving right edge of the log that every lag calculation is measured against.
- Committed offset — the offset a consumer group has acknowledged and persisted to
__consumer_offsets; the left edge of unread work and the value that drives lag. - Absolute lag —
LEO - committed_offsetper partition, summed across assignments for the group; necessary but not sufficient because record count alone hides whether 1,000 records means 100 seconds or 10 ms of delay. - Lag-time — the wall-clock age of the next unconsumed record; the metric users actually feel and the right SLO target for user-facing topics like
inference-requests. - Rebalance storm — repeated, rapid flipping of consumer-group state that halts consumption during each rebalance; a top cause of growing lag even when capacity looks healthy.
Concepts
What lag actually measures
For each partition the broker tracks two cursors. The log-end-offset (LEO) is the next offset producers will write. The committed offset is what the group has acknowledged, persisted to __consumer_offsets. Absolute lag is LEO - committed_offset; the group's total lag sums across assigned partitions.
That integer is necessary but not sufficient: 1,000 records on a 10 rec/s topic is 100 seconds behind; the same on a 100,000 rec/s topic is 10 ms behind — invisible. The metric users feel is lag-time: the wall-clock age of the next unconsumed record.
Why lag is topic-specific for AI pipelines
Lag tolerance is set by what the topic feeds. embeddings-ingest feeds the vector index — lag means new docs are not yet searchable, so RAG returns stale context. inference-requests queues user requests for GPUs — a 4,000-record lag at 200 req/s is a 20-second user-visible latency spike. SLOs follow: seconds for inference-requests, minutes for embeddings-ingest, hours for control-plane topics like model-events.
Uneven lag across partitions of the same topic is itself diagnostic — it points at hot keys or a failed instance, not raw capacity.
The four causes of chronic lag
When lag is consistently high or growing, the cause is almost always one of four things:
- Too few consumer instances. 12 partitions, 4 consumers, 100 ms per message = peak 120 rec/s. Producer rate above that grows lag forever. Fix: scale to the partition count, then add partitions carefully (new partitions break key-based ordering).
- Slow per-message processing. The most common AI cause: an embedding call that took 50 ms in dev now takes 800 ms because the model server is overloaded. Profile, externalize blocking calls, or batch.
- Downstream backpressure. Consumer is fast but writes to a rate-limited vector DB; the poll loop blocks. Fix at the downstream layer — adding consumers multiplies pressure on the bottleneck.
- Rebalance storms. Consumers join and leave faster than the coordinator can finish a rebalance; no consumption happens during one. Causes:
session.timeout.mstoo low, processing exceedingmax.poll.interval.ms, or pod churn (HPA flap, OOMKills, liveness failures).
Two alert classes that map to user pain
Absolute lag catches unbounded growth — producer rate exceeds consumer rate. Lag-time catches the case where record count is small but the topic is so slow that small lag still means minutes of delay. A third signal, member-count flap, catches rebalance storms before lag visibly grows. The Prometheus rules in Code Walkthrough encode all three.
Slow-consumer response runbook
When KafkaConsumerLagRecords or KafkaConsumerLagTime pages, work this checklist top-down:
- Confirm and classify (60 s). Run
kafka-consumer-groups.sh --describe --group $GROUP --state. Concentrated per-partition lag points at instance failure or hot keys; uniform lag points at a producer surge or downstream bottleneck. - Scale up consumers (never above partition count — extras idle):
kubectl scale deploy/embedding-worker --replicas=<partitions>. If lag keeps growing pastreplicas == partitions, you have a per-message-cost problem. - Rewind offset (only when lag is unrecoverable AND staleness is acceptable): stop consumers,
--reset-offsets --to-latest --execute, restart. This drops records on the floor — document what was skipped and what downstream artifacts need backfill. - Replay from a known-good offset for wrong-offset bugs: create a parallel group reading from a chosen timestamp, run the fixed consumer there, verify, cut over. Never replay into the live group.
Lag is also often downstream of broker problems — keep under-replicated partitions, broker disk usage, and a healthy __consumer_offsets on the same dashboard.
Code Walkthrough
Building on the lag-time and rebalance concepts above, this section walks through two concrete implementations: the lag collector (computing both absolute lag and lag-time without poisoning the production group) and the alert rules (absolute lag, lag-time, rebalance storm) that fire from the resulting metrics.
Code snippetpython
1from confluent_kafka import Consumer, TopicPartition 2from confluent_kafka.admin import AdminClient, ConsumerGroupTopicPartitions 3import time 4 5def collect_group_lag(bootstrap, group_id): 6 admin = AdminClient({"bootstrap.servers": bootstrap}) 7 c = Consumer({"bootstrap.servers": bootstrap, 8 "group.id": f"lag-probe-{int(time.time())}", 9 "enable.auto.commit": False}) 10 resp = admin.list_consumer_group_offsets( 11 [ConsumerGroupTopicPartitions(group_id)])[group_id].result(timeout=15) 12 out = [] 13 for tp in resp.topic_partitions: 14 _, log_end = c.get_watermark_offsets( 15 TopicPartition(tp.topic, tp.partition), timeout=5.0, cached=False) 16 lag = max(0, log_end - tp.offset) 17 lag_s = None 18 if lag > 0: 19 c.assign([TopicPartition(tp.topic, tp.partition, tp.offset)]) 20 msg = c.poll(timeout=2.0) 21 if msg and not msg.error() and msg.timestamp()[0] != 0: 22 lag_s = max(0.0, time.time() - msg.timestamp()[1] / 1000.0) 23 out.append((tp.topic, tp.partition, tp.offset, log_end, lag, lag_s)) 24 c.close() 25 return out
The probe uses a unique group.id so it never joins the production group's rebalance, and enable.auto.commit=False so it cannot write to __consumer_offsets. cached=False forces a metadata round-trip — cached watermarks turn real alerts into false negatives. In production, prefer kafka_exporter as a sidecar exposing the same family of metrics; reach for this collector when you need lag-time alongside absolute lag in one scrape.
Code snippetyaml
1groups: 2 - name: kafka-consumer-lag 3 rules: 4 - alert: KafkaConsumerLagRecords 5 expr: sum by (consumergroup, topic) (kafka_consumergroup_lag) > 5000 6 for: 2m 7 labels: { severity: page, topic_class: inference } 8 - alert: KafkaConsumerLagTime 9 expr: max by (consumergroup, topic) (kafka_consumergroup_lag_seconds) > 30 10 for: 1m 11 labels: { severity: page, topic_class: inference } 12 - alert: KafkaRebalanceStorm 13 expr: rate(kafka_consumergroup_members[5m]) > 0.05 14 for: 3m 15 labels: { severity: page }
The three rules map one-to-one onto the alert classes from Concepts: record-count growth, lag-time SLO breach, and member-count flap. Always use for: — a 30-second spike was self-healing; a two-minute one is an incident.
You'll know it works when (a) a synthetic producer-pause makes KafkaConsumerLagRecords fire within ~2 minutes and clears after consumers catch up, (b) KafkaConsumerLagTime fires independently of record count on a slow topic, and (c) bouncing a consumer pod three times in five minutes trips KafkaRebalanceStorm.
Do's and Don'ts
Do's
- ✓Do alert on lag-time, not just record count — users feel time, not records; page humans only on lag-time SLO breach for user-facing topics.
- ✓Do use a separate
group.idfor lag probing — sharing the production coordinator session pollutes rebalances and can re-commit offsets. - ✓Do treat
kafka_consumergroup_membersflap as an incident — three drops in five minutes is a rebalance storm and consumption is halted during each.
Don'ts
- ✗Don't scale consumers above partition count — extras sit idle; if lag keeps growing at
replicas == partitions, the bottleneck is per-message cost, not parallelism. - ✗Don't replay into the live consumer group — it has already moved past those offsets; spin up a parallel group, verify, then cut over.
- ✗Don't ignore
__consumer_offsetshealth — if it goes under-replicated, every group's commit can stall and your lag metrics start lying.
This lesson is free to read. Its 3 hands-on labs — real code, in a cloud IDE — are part of the GenAI Platform Engineering subscription.
From · cancel anytime
More free lessons in Data Infrastructure Essentials for GenAI
- Ch 1Integrate pgvector with SQLAlchemy ORM
- Ch 3Use Redis pub/sub for real-time event broadcasting
- Ch 3Build rate limiting with Redis sorted sets
- Ch 3Monitor Redis performance and memory
- Ch 5Monitor Kafka with consumer lag metricsYou are here
- Ch 8Define Argo Workflow templates for data processing
- Ch 10Deploy data services on Kubernetes with StatefulSets