OutputValues icon
Request icon
TopicList icon
If icon
Set icon
Log icon
SlackIncomingWebhook icon
Pause icon
Loop icon
Fail icon
Schedule icon

Kafka Schema Registry Orphaned Subject Cleanup Guard

Detect Schema Registry subjects with no live Kafka topic, hard delete them behind an approval gate, then re-list and fail if any targeted subject remains.

Categories
DataInfrastructure

A Confluent Schema Registry under the TopicNameStrategy keeps one subject per topic field: <topic>-value for the record value and <topic>-key for the key. The subjects are not tied to the topic lifecycle. Drop the topic and its subjects stay registered, holding their schema ids and version history. Over time a cluster collects subjects for topics nobody runs any more. They clutter GET /subjects, they confuse a compatibility check that reads the latest version of a dead subject and they pin schema ids that can never be reused.

Soft deletes leave the same debris. DELETE /subjects/<subject> without permanent=true only hides the subject from the default listing. The schema ids stay allocated until a second call with permanent=true runs. That second call is easy to forget.

This blueprint lists the registered subjects, lists the already soft deleted ones, reads the live Kafka topics and works out which subjects are orphaned. It tracks how long each has been orphaned in a KV cursor, so a subject deletes only after its topic has been gone past a threshold. Deletion is off by default, gated behind auto_remediate, dry_run and a human approval. After the delete it re-lists the subjects and fails the run if any targeted subject 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.
  • A subject whose topic still exists is never a target. The live topic set comes straight from the broker on every run.
  • The age cursor has no ttl, so a subject has to stay orphaned across the whole window before it is eligible. A topic dropped and recreated inside the window keeps its subjects.
  • protected_subjects are never deleted, even when their topic is gone.
  • Only subjects following the <topic>-value or <topic>-key naming are matched against topics. A custom named subject is never treated as topic bound.

How it works

  1. freeze captures one timestamp for the run, so the detect, cursor and age steps all agree on now.
  2. list_subjects (io.kestra.plugin.core.http.Request) reads GET /subjects, the subjects served right now.
  3. list_all_subjects reads GET /subjects?deleted=true, which also returns the soft deleted ones. The soft deleted set is the difference between the two lists.
  4. list_topics (io.kestra.plugin.kafka.TopicList, read-only) returns the live topics on the cluster.
  5. analyze builds the orphan set. A subject is orphaned when it follows the topic name strategy and its topic is not in the live set or when hard_delete_soft_deleted is true and it is already soft deleted. Protected subjects are dropped.
  6. route (io.kestra.plugin.core.flow.If) branches on whether any orphan exists.
    • When none exist, clear_cursor resets the first seen record and all_clean logs that there is nothing to clean. No alert.
    • When some exist:
      • cursor_merge and save_cursor keep the first time each still orphaned subject was seen in the KV key kafka_sr_orphans_<registry>, with no ttl.
      • split separates the orphans past min_age_minutes from the ones 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 subject is past the threshold.
        • approve (io.kestra.plugin.core.flow.Pause) waits for a resume carrying approved.
        • When approved, remediate_delete (io.kestra.plugin.core.flow.Loop, concurrencyLimit: 1) soft deletes then permanently deletes each targeted subject, reverify re-lists and verify_result fails the run if any targeted subject is still present.
        • When not approved, not_approved logs that nothing was deleted.
  7. errors posts to Slack on any failure, for example an unreachable registry or a subject that survived the delete.

What you get

  • A clean inventory of orphaned subjects, split into topic gone and already soft deleted, separated from the subjects still in use.
  • An age gate that will not delete a subject whose topic was only just removed.
  • 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 a shared registry where topics come and go faster than anyone prunes the subjects.
  • On-call engineers who want the dead subjects gone without running DELETE ...?permanent=true against production by hand.
  • Teams evaluating the Kafka plugin who want a worked example of TopicList paired with the registry REST API behind an approval gate.

Why orchestrate this with Kestra

A one line curl can delete a subject. It cannot remember how long the subject has been orphaned, cross check the live topic set, hold the delete for a second person or prove afterwards that the subject is really gone. Kestra adds the schedule that watches the registry, the KV cursor that measures the orphaned age across runs, the Pause that turns a permanent 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 Confluent Schema Registry 7.9.0 and a local HTTP sink standing in for Slack. Four topics (orders, payments, shipments, inventory) each had Avro subjects registered under the TopicNameStrategy, plus a non strategy subject my-custom-subject that must never be matched. State was confirmed with GET /subjects plus GET /subjects?deleted=true before and after each run.

Run Inputs Result
1 defaults, clean registry 0 orphans. All 6 subjects map to a live topic or are non strategy. Nothing deleted. No alert
2 topic shipments dropped, payments-value soft deleted, min_age_minutes: 0 2 orphans reported (shipments-value topic gone, payments-value soft deleted), both past threshold. dry_run true so nothing deleted. ?deleted=true unchanged
3 same, auto_remediate: true, dry_run: true Same report, no Pause, nothing deleted. Registry unchanged
4 min_age_minutes: 120, fresh cursor, auto_remediate: true, dry_run: false 2 orphans reported, both held as too recent, 0 past threshold, no Pause. Registry unchanged
5 min_age_minutes: 0, auto_remediate: true, dry_run: false, approved Paused, resumed with approved=true. shipments-value soft then hard deleted, payments-value hard deleted. Re-list confirmed gone, ?deleted=true dropped from 6 subjects to 4
6 topic inventory dropped, inventory-value set read only, approved Delete rejected by read only mode, re-list still showed inventory-value, run failed and alerted
7 registry URL unreachable list_subjects failed, run failed, errors alert sent

Across every run the subjects whose topic still exists (orders-value, orders-key) and the non strategy subject (my-custom-subject) were never flagged.

Prerequisites

  • A Kafka cluster plus a Confluent Schema Registry reachable from the Kestra worker. The registry keeps its state in a Kafka topic, so it cannot run without a broker. This is why the blueprint is not a demo.
  • A registry user allowed to list subjects and, for remediation, delete them. A broker principal allowed to list topics.
  • Subjects named with the default TopicNameStrategy (<topic>-value, <topic>-key). A different strategy needs the match in analyze adjusted.

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
schema_registry_url http://localhost:8081 Registry base URL, no trailing slash
bootstrap_servers localhost:9092 Brokers, read the live topic set
min_age_minutes 1440 How long a subject must stay orphaned before it can be deleted
hard_delete_soft_deleted false Also treat already soft deleted subjects as orphans
protected_subjects [] Subject names never deleted
auto_remediate false Allow the delete
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 schema_registry_url plus bootstrap_servers.
  3. Run it with the defaults and read the orphaned subjects per registry.
  4. Set min_age_minutes to how long a topic must be gone 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 cleanup at a different registry from the one that schedules the flow, by making schema_registry_url the target registry.
  • Export each subject's schema versions to object storage before deleting, so a wrongly cleaned subject can be rebuilt.
  • Set hard_delete_soft_deleted to true to also reclaim the ids of subjects an operator soft deleted and forgot.
  • Match a different subject naming strategy by changing the topic extraction in analyze, for example RecordNameStrategy.
  • Raise the Slack message to a channel with a link to the paused execution, so approval happens from chat.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.