BitBootcampBitBootcamp ← all explainers 🔄 Review ⚙ Settings

Drill 83 — The Consumer That Fell Behind

Theme: Queues & Backpressure · Format: 20-min drill · Difficulty: core

The story

A payments platform runs a Kafka consumer group that reads every card transaction off a transactions topic, enriches each one with a synchronous call to a merchant-risk service, and writes the result to the store that feeds the fraud engine and the operations dashboards. Producers push a steady eight thousand transactions a second all day. The consumer group — three instances across twelve partitions — normally sits within a second of the tip of the log. The lag chart has been flat for a year, so nobody looks at it.

At 11 p.m. the merchant-risk service quietly degrades: a database behind it loses a read replica, and per-call latency drifts from fifteen milliseconds to four hundred. Nothing errors. The enrichment calls still return 200 OK — just slowly. Each consumer instance now spends most of its poll loop waiting on that call, so it commits offsets far slower than transactions arrive. A garbage-collection pause on one instance an hour later makes it worse. The gap between the tip of the log and the committed offset — the lag — starts to widen, a few thousand records a minute.

There is no alarm on lag. The dashboards are green because the records they do have look perfectly healthy; they're just old. The fraud engine keeps scoring, but it's scoring transactions from twenty minutes ago, then forty, then an hour. Every service is up. Every health check passes. The consumers are processing as fast as they can — they're simply processing the wrong hour.

By the time the first analyst arrives at 7 a.m. and notices the fraud queue is reviewing yesterday's dinner-time transactions, the group is six hours behind. Six hours of transactions have been authorized against stale risk scores, and the morning's real-time dashboards are showing a version of the business that ended before midnight.

⏸ Stop here and ask the room: every service was up, every health check green, and the consumers never stopped working. So how did the whole platform end up running six hours in the past?

Run it — facilitator script

Timing for a 20-minute session: 2 min story · 10 min staged questions · 3 min reveal · 5 min transfer.

The consumers never crashed and never errored. Where is the failure hiding?

What is the single number that would have caught this, and what does it measure?

Suppose lag alarms. Adding more consumer instances — does that always help?

The reveal

The enrichment slowdown was real but recoverable — the read replica came back on its own around 2 a.m. The damage was that nothing measured whether the pipeline was keeping up. The team added a consumer-lag SLI (both records-behind and seconds-behind) with a page at sustained lag, wired the consumer group to autoscale on lag up to the partition ceiling, raised the partition count so parallelism could actually grow, and added an end-to-end freshness monitor on the fraud store so "the newest record is older than N minutes" pages even if lag reporting itself breaks. On the next enrichment slowdown, lag crossed the threshold in four minutes, consumers scaled out, and the group recovered a small backlog before anyone noticed — instead of after six hours.

Why this happens — the mechanics

A consumer group is a queue with the depth hidden on the broker side. Lag is exactly produced_offset − committed_offset: it grows whenever consume rate falls below produce rate, and — like any queue under a persistent slowdown (Little's Law) — it grows without bound until the rates re-cross. Because the records the consumer has already processed look completely normal, every downstream health check stays green. Staleness is invisible from inside a record; you can only see it by comparing the record's age to now, and nothing in the default health-check vocabulary does that.

The organizational root is an ownership seam: the platform team owns "are the services up," the data team owns "is the fraud store correct," and nobody owns "is the consumer keeping up with the log." Lag is a broker-side metric that neither dashboard naturally surfaces, so a best-practice, healthy-looking deployment can drift hours into the past with every signal green. It's a slow-motion version of the unbounded-queue failure: the queue lives in Kafka instead of in the heap, so it never OOMs — it just silently serves the past.

Diagrams

Problem — how the failure unfolds:

sequenceDiagram
    autonumber
    participant P as Producers
    participant K as Kafka topic
    participant C as Consumer group
    participant D as Fraud + dashboards
    P->>K: 8k transactions/sec steady
    K->>C: poll batch
    Note over C: enrichment 15ms to 400ms<br/>plus a GC pause
    C--xK: commit offset slips<br/>behind the log tip
    Note over K: lag grows silently<br/>no alarm on lag
    C->>D: writes stale (old) results
    Note over D: dashboards green<br/>data is hours old
    Note over P,D: found at 7am by an analyst<br/>six hours behind

Solution — where the guardrails go:

flowchart TD
    A["Transactions to Kafka"] --> B["Consumer group<br/>processes batches"]
    B --> G1["Consumer-lag SLI<br/>records + seconds behind"]
    G1 -.-> AL["Page: lag over 60s<br/>for 5 minutes"]
    B --> G2["Autoscale on lag<br/>up to partition count"]
    B --> G3["Partition parallelism<br/>match key cardinality"]
    B --> G4["End-to-end freshness<br/>SLO on fraud store"]
    G4 -.-> AL
    AL --> H["On-call drains<br/>the backlog fast"]
    G2 --> H
    G3 --> H

What good looks like

The lesson

A service can be fully up and still be hours in the past: measure whether your consumers are keeping up, not just whether they're alive. Lag and end-to-end freshness are first-class SLIs — alarm on how far behind the data is, because "green" says nothing about "current."

Named concepts: consumer-lag SLI, autoscaling on lag, partition parallelism, end-to-end freshness SLO.

Real-world echoes

Transfer

Which of your streaming consumers or queue workers has no alarm on how far behind it is — and if its slowest dependency degraded tonight, how many hours could it fall into the past before a human noticed?

Make it a ticket: for every consumer group, export lag in records and seconds, page at 60 seconds behind for 5 minutes, add autoscaling on lag capped at partition count, and add an end-to-end freshness alarm at 2 minutes on the store it feeds.

Related