OutputValues icon
ConsumerGroupList icon
If icon
ConsumerGroupDescribe icon
Set icon
Log icon
SlackIncomingWebhook icon
Pause icon
ConsumerGroupDelete icon
Fail icon
Schedule icon

Kafka Stale Consumer Group Cleanup Guard

Detect empty idle Kafka consumer groups, delete them with ConsumerGroupDelete behind an approval gate, then re-list and fail if any targeted group remains.

Categories
DataInfrastructure

Dead applications leave consumer groups behind. The last member leaves, the group goes to state Empty with its committed offsets still stored and nothing ever removes it. Over months a cluster collects hundreds of these. They clutter kafka-consumer-groups.sh --list, they show up on lag dashboards reading against offsets no one consumes and they make the real groups harder to find.

Deleting them by hand is risky. A group that looks empty right now might be a consumer that is restarting or rebalancing. Deleting its group drops the committed offsets, so when it comes back it reprocesses or skips messages. The safe rule is to delete a group only after it has stayed empty long enough that it is clearly abandoned.

This blueprint lists every consumer group, keeps the ones that are Empty or Dead with zero active members and tracks how long each has been empty in a KV cursor. A group deletes only when it has been empty past min_idle_minutes, only when auto_remediate is true and dry_run is false and only after someone approves the paused execution. After the delete it re-lists the groups and fails the run if any targeted group is still there.

This blueprint was created by zkasuran.

Safety

  • Read-only by default. With the shipped inputs it only lists, reports and alerts. It changes nothing.
  • A delete needs three things at once: auto_remediate true, dry_run false and a human resuming the Pause with approved=true. An unanswered resume reads as not approved.
  • Only groups confirmed Empty or Dead with zero members are ever targeted. Kafka itself refuses to delete a group with active members, which is a second net under the detector.
  • The age cursor has no ttl, so a group has to be seen empty across the whole window before it is eligible. A brief empty window from a restart never crosses the line.
  • protected_groups are never deleted even when empty.

How it works

  1. freeze captures one timestamp for the run, so the detect, cursor and age steps all agree on now.
  2. list_all (io.kestra.plugin.kafka.ConsumerGroupList, read-only) returns every group with its state.
  3. candidates keeps the groups in state Empty or Dead that are not in protected_groups.
  4. route (io.kestra.plugin.core.flow.If) branches on whether any candidate exists.
    • When none exist, clear_cursor resets the first-seen record and all_active logs that there is nothing to clean. No alert.
    • When some exist:
      • describe_candidates (io.kestra.plugin.kafka.ConsumerGroupDescribe, read-only) reads members and offsets, then confirmed keeps only the ones with zero active members.
      • cursor_merge and save_cursor keep the first time each still-empty group was seen in the KV key kafka_stale_groups_<cluster>, with no ttl.
      • targets splits the confirmed groups into those empty longer than min_idle_minutes and those held back as too recent.
      • log_findings logs the split and alert posts it to Slack.
      • remediate_gate runs only when auto_remediate is true, dry_run is false and at least one group is past the threshold.
        • approve (io.kestra.plugin.core.flow.Pause) waits for a resume carrying approved.
        • When approved, delete_groups (io.kestra.plugin.kafka.ConsumerGroupDelete) deletes the targeted groups, reverify re-lists and verify_result fails the run if any targeted group is still present.
        • When not approved, not_approved logs that nothing was deleted.
  5. errors posts to Slack on any failure, for example an unreachable broker or a group that survived the delete.

What you get

  • A clean inventory of abandoned consumer groups, separated from the ones still in use.
  • An age gate that will not delete a group that was empty only for a moment.
  • Deletion that is off by default, gated behind auto_remediate, dry_run and a human approval.
  • Proof the cleanup landed: a re-list after the delete that fails the run if a target survived.

Who it's for

  • Platform teams running shared Kafka clusters where old services keep leaving groups behind.
  • On-call engineers who want the empty groups gone without typing --delete against a live cluster by hand.
  • Teams evaluating the Kafka plugin who want a worked example of ConsumerGroupList, ConsumerGroupDescribe and ConsumerGroupDelete with gates around the delete.

Why orchestrate this with Kestra

kafka-consumer-groups.sh --delete removes a group straight away. It does not remember how long the group has been empty, hold the delete for a second person or check afterwards that the group is really gone. Kestra adds the schedule that watches the groups, the KV cursor that measures the idle age across runs, the Pause that turns a delete into an approved change, the re-list that proves the result and a history of every cleanup.

Tested end to end

On Kestra 2.0.5 OSS against Apache Kafka 3.9.1 (KRaft, single node), with a local HTTP sink standing in for Slack. The topic orders had 3 partitions and 30 messages. Three groups (legacy-exporter, old-reporting, defunct-worker) read to the end then stopped, so each went to state Empty. One group (live-processor) kept a live consumer and stayed Stable. States were confirmed with kafka-consumer-groups.sh --list and --describe --state before and after each run.

Run Inputs Result
1 defaults, min_idle_minutes: 0 3 empty groups reported, live-processor not listed. Nothing deleted (dry_run true). --list unchanged
2 auto_remediate: true, dry_run: true Same report, no Pause, nothing deleted. --list unchanged
3 auto_remediate: true, dry_run: false, min_idle_minutes: 120, fresh cursor 3 reported, all held as too recent, 0 past threshold, no Pause. --list unchanged
4 auto_remediate: true, dry_run: false, min_idle_minutes: 0, approved Paused, resumed with approved=true. 3 groups deleted, re-list confirmed gone. --list shows only live-processor
5 same, resumed without approval Paused, resumed with no answer. Nothing deleted. --list unchanged
6 approved, a consumer rejoined one target during the pause Delete left that group behind, re-list still showed it, run failed and alerted
7 unreachable broker Run failed, errors alert sent

Prerequisites

  • A Kafka cluster reachable from the Kestra worker, with a principal allowed to list and describe groups and, for remediation, delete them.
  • Stop a group's consumers before you expect it to be cleaned. A group with active members is not Empty, so it is never targeted.

Secrets

  • SLACK_WEBHOOK_URL: used by alert and the errors block. In Kestra OSS, provide it as the base64-encoded environment variable SECRET_SLACK_WEBHOOK_URL.

Inputs

Input Default Purpose
bootstrap_servers localhost:9092 Brokers
min_idle_minutes 1440 How long a group must stay empty before it can be deleted
protected_groups [] Group ids never deleted
auto_remediate false Allow ConsumerGroupDelete
dry_run true Must be false for a delete to apply

Quick start

  1. Add the SLACK_WEBHOOK_URL secret.
  2. Import the flow and set bootstrap_servers.
  3. Run it with the defaults and read the empty groups per cluster.
  4. Set min_idle_minutes to how long a group must be abandoned before you trust the delete, for example a day.
  5. Run it with auto_remediate: true and dry_run: false, then resume the paused execution with approved=true.
  6. Enable the daily_scan trigger, which always runs in safe mode.

How to extend

  • Point the delete at a different cluster from the one that schedules the flow, by making bootstrap_servers the target cluster.
  • Archive each group's committed offsets to the KV store or object storage before deleting, so a wrongly cleaned group can be rebuilt.
  • Raise the Slack message to a channel with a link to the paused execution, so approval happens from chat.
  • Treat a group whose offsets are far behind the log end as a separate finding, since an empty group with large lag can mean a crashed consumer rather than a retired one.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.