SD System Design In Depth
0%
lesson system-designqueuesbackpressure

Queue lag

Module 4.2: Asynchronous Workflows & Partitioned Logs Updated Sep 17, 2026
📄 Cheat Sheet
BEWARE of Consumer Lag! Event Driven Architecture Monitoring
CodeOpinion BEWARE of Consumer Lag! Event Driven Architecture Monitoring Watch on YouTube ↗

CodeOpinion (Derek Comartin) explains consumer lag as an operational signal: why it grows, what to measure (including message age), and how to react. It is directly on topic.

0:00 / --:--

Under the Hood: Consumer Lag & Little's Law

In distributed message processing, Consumer Lag is the delta between the latest message produced to a partition (the latest offset a consumer can read: the high watermark, or the last stable offset under read_committed; lag tools often label this the log-end offset) and the latest message offset committed by the consumer group:

Consumer_Lag = Log_End_Offset - Current_Consumer_Offset

Offset lag is one useful indicator, but oldest-event age, arrival and service rates, retry volume, partition skew, and downstream latency are often needed to explain user-visible delay. For a stable system over a defined window, Little's Law (L = λ W) relates average in-flight work, throughput, and average time in system. If arrivals exceed service capacity, the system is not stable: backlog and age continue growing until capacity increases, intake is limited, or work is shed.

Consumer Lag and Auto-Scaling Loop

flowchart LR Producers["Producers (Burst: 50,000 msg/sec)"] --> Partition["Kafka Topic Partition (High Watermark: 95,000)"] Partition -->|"Lag = 45,000"| Consumer["Consumer Worker Pool (Current Offset: 50,000)"] Consumer -->|"Report Metrics"| Prom["Prometheus (kafka_consumergroup_lag)"] Prom -->|"Threshold Exceeded (> 10,000)"| KEDA["KEDA / Kubernetes HPA"] KEDA -->|"Scale Pods (Workers: 2 -> 16)"| Consumer

Backpressure & Flow Control Strategies

When downstream consumers cannot keep up with upstream producers, the system must exert Backpressure rather than crashing:

  • Pull-Based Flow Control: Unlike push delivery with no limit on unacknowledged messages (RabbitMQ bounds this with consumer prefetch via basic.qos, and SQS is itself pull-based), Kafka consumers pull batches of records using poll(Duration timeout), with the batch size capped by the max.poll.records setting. Workers dictate their own ingestion rate based on available processing capacity.
  • Dead-Letter Queues (DLQ): If a poisoned message repeatedly causes worker crashes or exceeds retry thresholds, the consumer routes it to an isolated DLQ topic and advances its offset, preventing a single poisoned message from stalling the entire consumer group.

Worked example: how long until we catch up?

A topic receives 20,000 messages per second. Ten consumers each process 2,500 per second, 25,000 in total. A deploy pauses consumption for 5 minutes.

  • Backlog built up: 20,000 x 300 s = 6,000,000 messages.
  • Net drain rate: 25,000 - 20,000 = 5,000 per second.
  • Time to drain: 6,000,000 / 5,000 = 1,200 s = 20 minutes, four times longer than the outage.
  • Scale to 20 consumers (if there are at least 20 partitions): drain at 50,000 - 20,000 = 30,000 per second, done in 200 s.
  • With only 12 partitions, 12 consumers give 30,000 capacity and a 10,000 per second net drain, so 10 minutes. Consumers beyond 12 do nothing.

Little's law cross-check: at steady state, 20,000 messages per second arriving with an average of 50 ms in the system means about 1,000 messages in flight. A lag of 6 million means that much excess backlog on top of normal.

Metrics that matter

MetricWhat it tells youCaveat
Offset lag per partitionMessages behindNot comparable across topics with different processing costs
Time lag (age of the oldest unconsumed message)User-visible delayNeeds timestamps. Burrow and Kafka Lag Exporter estimate it
Consume rate against produce rateWhether lag is shrinkingLook at trends, not snapshots
Rebalance countGroup instabilityFrequent rebalances stall consumption
DLQ and retry topic ratePoison or failing messagesRetries can hide lag in another topic

Levers when lag grows

  • Scale out, up to the partition count. Adding partitions later remaps keys, so plan for peak throughput up front.
  • Batch downstream writes: one bulk insert of 500 rows instead of 500 single inserts.
  • Fix slow handlers: tune max.poll.records and max.poll.interval.ms so slow batches do not trigger rebalances.
  • Shed or defer: drop low-value events, such as analytics pings, during incidents.
  • Autoscale on lag: KEDA's Kafka scaler targets lag per partition. Cap maximum replicas at the partition count.
Check yourself Question 1 of 3
Time to drain apply
From: Worked example: how long until we catch up?

A topic receives 30,000 messages per second. Eight consumers each handle 5,000 per second. A deploy pauses consumption for 10 minutes. How long does the backlog take to drain once consumers resume?

Queue Backpressure & Lag
Live
Queue Depth 0 / 100
Lag: 0.0s
Status: OK

Key Takeaways

Consumer lag measures the delta between the broker high watermark and consumer committed offset.
Pull-based consumption naturally enforces backpressure by allowing workers to control their ingestion rate.
Automated scaling (via KEDA/Kubernetes HPA) dynamically adds worker pods when consumer lag breaches thresholds.
Was this lesson helpful? Spot an issue or have a suggestion? Let us know on GitHub.
Report an issue