New to Kestra?
Use blueprints to kickstart your first workflows.
Detect empty idle Kafka consumer groups, delete them with ConsumerGroupDelete behind an approval gate, then re-list and fail if any targeted group remains.
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.
auto_remediate true, dry_run false and a human resuming the Pause with approved=true. An unanswered resume reads as not approved.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.protected_groups are never deleted even when empty.freeze captures one timestamp for the run, so the detect, cursor and age steps all agree on now.list_all (io.kestra.plugin.kafka.ConsumerGroupList, read-only) returns every group with its state.candidates keeps the groups in state Empty or Dead that are not in protected_groups.route (io.kestra.plugin.core.flow.If) branches on whether any candidate exists.clear_cursor resets the first-seen record and all_active logs that there is nothing to clean. No alert.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.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.not_approved logs that nothing was deleted.errors posts to Slack on any failure, for example an unreachable broker or a group that survived the delete.auto_remediate, dry_run and a human approval.--delete against a live cluster by hand.ConsumerGroupList, ConsumerGroupDescribe and ConsumerGroupDelete with gates around the delete.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.
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 |
Empty, so it is never targeted.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.| 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 |
SLACK_WEBHOOK_URL secret.bootstrap_servers.min_idle_minutes to how long a group must be abandoned before you trust the delete, for example a day.auto_remediate: true and dry_run: false, then resume the paused execution with approved=true.daily_scan trigger, which always runs in safe mode.bootstrap_servers the target cluster.