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

Kafka Schema Registry Subject Version Pruning Guard

Soft delete the oldest Schema Registry subject versions down to a keep_n target behind an approval gate, then re-read and fail if the subject is still over.

Categories
DataInfrastructure

A Confluent Schema Registry subject gains a version every time its schema changes. The registry never removes one. A subject that has evolved weekly for two years carries more than a hundred active versions. Nothing in the registry will ever thin that list.

Nothing breaks, which is why it goes unnoticed. The cost is operational. The _schemas storage topic is compacted but never pruned, so it keeps every version of every subject forever and a new registry replica replays all of it on startup. GET /subjects/<subject>/versions turns into a wall of numbers that no one can read. And once the list is long enough, nobody can say which old versions still matter, so nobody dares touch any of them.

This blueprint prunes the history down to the newest keep_n versions per subject, oldest first, with a SOFT delete. A soft delete removes the version from the active list but leaves the global schema id resolvable through GET /schemas/ids/<id>, so a consumer that is still reading old messages whose headers carry that id keeps working. The newest version is never a target. After the prune the flow re-reads the subject. It also re-resolves every schema id it just soft deleted, so the run fails both when the subject is still over target and when a soft delete turned out to behave like a hard delete.

This blueprint was created by zkasuran.

Safety

  • Read-only by default. With the shipped inputs it lists subjects, lists versions, 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.
  • The delete is a soft delete. The version leaves the active list, the schema stays retrievable by its global id, so a consumer holding that id can still deserialise an old message.
  • permanent_delete offers the hard delete, which does free the ids and is unrecoverable. It is off by default and needs a second field on the same approval, confirm_permanent=true, so one careless resume cannot trigger it.
  • The newest version of a subject is never deletable. The registry itself will happily soft delete it. When it is the only one left the whole subject disappears from GET /subjects, so this flow applies the rule instead. If keep_n is 0 the run fails with the subject names rather than pruning anything.
  • Only subjects backing a live Kafka topic are in scope, read from the broker on every run, so a subject whose topic is gone keeps its whole history.
  • exempt_subjects are never read and never changed.
  • keep_n is an operator judgement, not an analysis. The registry cannot tell you whether a version from last year is still referenced by messages sitting in a topic with long retention. Pick keep_n from your longest topic retention and your slowest consumer, keep the soft delete and leave permanent_delete off.

How it works

  1. list_subjects (io.kestra.plugin.core.http.Request) reads GET /subjects.
  2. list_topics (io.kestra.plugin.kafka.TopicList, read-only) returns the live topics on the cluster.
  3. scope keeps the registered subjects that back a live topic under the TopicNameStrategy (<topic>-value, <topic>-key) or the explicit subjects list, minus exempt_subjects. Internal topics are dropped.
  4. in_scope (io.kestra.plugin.core.flow.If) stops with no_subjects when nothing is in scope.
  5. audit (io.kestra.plugin.core.flow.Loop, concurrencyLimit: 1) reads GET /subjects/<subject>/versions per subject and folds the active version list into a KV accumulator.
  6. assess marks a version as a target when fewer than keep_n versions sit above it. That keeps the newest keep_n whatever gaps the numbering has. With keep_n 0 it marks the subject unsafe, because the newest version would be a target.
  7. route branches on whether any subject is over target.
    • When none are, all_within logs that there is no history to prune. No alert.
    • When some are, log_findings logs the per subject split and alert posts it to Slack.
  8. safety_gate fails the run through refuse_latest if the target set reaches a newest version. Nothing is deleted.
  9. remediate_gate runs only when auto_remediate is true and dry_run is false. Otherwise report_only logs what would have been pruned.
    • approve (io.kestra.plugin.core.flow.Pause) waits for a resume carrying approved, plus confirm_permanent when permanent_delete is set.
    • remediate_prune (Loop, concurrencyLimit: 1) walks the targets oldest first. Per version it reads the version to capture its global schema id, soft deletes it, hard deletes it only when both permanent flags are set, then re-resolves that schema id and records the answer.
    • verify re-reads the active versions of every pruned subject.
    • verify_assess counts two independent failures. One is a subject still over keep_n. The other is a soft deleted version whose schema id stopped resolving.
    • verify_result fails the run through prune_failed on either, otherwise pruned_ok logs the new counts.
  10. errors posts to Slack on any failure, for example an unreachable registry, a refused prune or a lost schema id.

What you get

  • A per subject view of how deep the version history actually is, scoped to the subjects that still back a live topic.
  • A prune that keeps the newest keep_n and walks the rest oldest first, with the newest version protected by a rule the registry does not enforce itself.
  • A soft delete by default, with the hard delete behind its own input and its own confirmation.
  • Proof the prune was safe: every soft deleted schema id is re-resolved by global id after the delete. The run fails if one of them stopped answering.
  • Proof the prune landed: a re-read of each subject that fails the run if it is still over target.

Who it's for

  • Platform teams running a shared registry where a few long lived subjects carry years of versions.
  • Teams whose registry replicas are slow to start because the _schemas topic replay has grown.
  • On-call engineers who want a reviewed way to thin a subject's history instead of hand running DELETE /subjects/<s>/versions/<v> against production.
  • Anyone who wants a worked example of the Kafka plugin paired with the registry REST API behind an approval gate.

Why orchestrate this with Kestra

A loop of curl calls can delete versions. It cannot scope itself to the subjects that still back a live topic, refuse to touch the newest version, hold the delete for a second person or prove afterwards that the schema ids it removed from the active list are still resolvable. Kestra adds the schedule that watches the registry, the approval that turns a bulk delete into a reviewed change, the re-read that proves the result and a run history that says exactly which versions went and when.

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. Topics orders, payments and invoices were live. orders-value was set to NONE compatibility and loaded with 8 versions (global schema ids 1 to 8). payments-value held 2 versions and is the precision control. Every result was confirmed by reading GET /subjects/<s>/versions and GET /schemas/ids/<id> directly, before and after each run.

Run Inputs Result
1 keep_n: 20, defaults 2 subjects audited, 0 over target. Nothing deleted, no alert
2 keep_n: 3 orders-value reported at 8 active with versions 1 to 5 as targets, payments-value not flagged at 2. dry_run true so nothing deleted, GET /subjects/orders-value/versions still [1,2,3,4,5,6,7,8]
3 keep_n: 3, auto_remediate: true, dry_run: true Same report, no Pause, registry unchanged
4 keep_n: 0, auto_remediate: true, dry_run: false refuse_latest failed the run naming orders-value and payments-value, nothing deleted, errors alert sent. The refusal lands before the Pause, so an approver is never offered it
5 keep_n: 3, auto_remediate: true, dry_run: false, resumed with no answer Paused, resumed empty, not_approved logged, registry unchanged
6 keep_n: 3, auto_remediate: true, dry_run: false, approved=true Versions 1 to 5 soft deleted oldest first. Active list went [1,2,3,4,5,6,7,8] to [6,7,8], payments-value still [1,2], latest still v8. Schema ids 1 to 5 all answered 200 on GET /schemas/ids/<id> after the delete
7 keep_n: 1, approved, subject put in READONLY mode during the Pause Registry rejected the deletes, the re-read still showed 3 active against a target of 1, prune_failed failed the run and errors alerted
8 subjects: ["invoices-value"], keep_n: 2, permanent_delete: true, resumed with approved=true only not_approved. The hard delete is unreachable without confirm_permanent, invoices-value still [1,2,3,4]
9 same plus confirm_permanent=true Versions 1 and 2 hard deleted. Active went [1,2,3,4] to [3,4] and ?deleted=true also returned [3,4]. Schema ids 13 and 14 answered 404, ids 15 and 16 still 200. The run reported 2 ids freed and did not fail, because the hard delete was asked for
10 keep_n: 3, the same inputs as run 2 3 subjects audited, 0 over target. The detector went from 1 over with 5 targets to 0 over on identical inputs
11 topic invoices deleted on the broker, keep_n: 1 Only 2 subjects audited. invoices-value was left out of scope entirely even though it still held 2 active versions, so its history was never read or touched
12 registry URL unreachable list_subjects failed, run failed, errors alert sent
13 every candidate in exempt_subjects no_subjects logged, nothing read, nothing changed

The soft delete semantics were measured rather than assumed. After a soft delete of a version, GET /schemas/ids/<id> returns 200 with the schema and the version still appears under ?deleted=true. After a hard delete of the same version the id answers 404 Schema <id> not found, ?deleted=true does not bring it back and the version is gone from the history too. That difference is the whole reason the default here is a soft delete.

One more measurement worth stating, because it is why the newest version rule lives in this flow and not in the registry. Schema Registry 7.9.0 soft deletes the newest version of a subject on request without complaint. When that was the last active version the subject itself stopped answering GET /subjects/<s>/versions and dropped out of GET /subjects. The registry offers no protection here at all.

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 read subjects and versions and, for remediation, delete versions. A broker principal allowed to list topics.
  • Subjects named with the default TopicNameStrategy. A different strategy needs the subjects input filled in explicitly.

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 that scopes the prune
keep_n 20 Active versions kept per subject. Must be 1 or more
subjects [] Empty means every subject backing a live topic
exempt_subjects [] Subjects whose history is kept whatever its length
permanent_delete false Hard delete, which frees the schema ids and is unrecoverable
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 how deep each subject's history is.
  4. Pick keep_n from your longest topic retention and your slowest consumer, then run again with dry_run true to see the exact version numbers that would go.
  5. Run it with auto_remediate: true and dry_run: false, then resume the paused execution with approved=true.
  6. Enable the weekly_scan trigger, which always runs in safe mode.

How to extend

  • Export each targeted version's schema to object storage before the delete, so a wrongly pruned version can be re-registered.
  • Set a different keep_n per subject group by running the flow once per group with the subjects input filled in.
  • Replace the fixed keep_n with an age rule by reading the version metadata and keeping anything newer than a cutoff.
  • Cross check against consumer lag before pruning, so a subject with a consumer group far behind is held back.
  • 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.