New to Kestra?
Use blueprints to kickstart your first workflows.
Find Pulsar subscriptions with zero consumers holding a backlog, then skip_all or unsubscribe behind a dry run, a pause and a live re-read.
A Pulsar subscription with zero consumers does not stop costing anything. The broker keeps every unacknowledged message for it, the backlog grows without bound. The ledgers behind those messages can never be reclaimed either, because Pulsar will not delete a ledger that any subscription still references. A service decommissioned months ago can hold terabytes this way. The only sign is a storage graph that climbs while nothing reads. This blueprint finds those subscriptions through the Pulsar admin REST API, holds them behind an age window so a consumer that is merely restarting is never touched, then clears them behind a dry run and a human approval before reading the live topic stats again to prove the backlog actually went away.
This blueprint was created by zkasuran.
The two actions are not interchangeable, so remediation_action is an explicit input and it defaults to the less destructive one.
skip_all acknowledges every pending message for that subscription. The data is gone for that consumer group, the ledgers stop being pinned. The subscription survives with an empty backlog, so a consumer that reconnects later keeps its identity and receives new messages from that point on.
unsubscribe deletes the subscription outright. The ledgers stop being pinned in the same way, but the cursor goes with it, so a consumer that reconnects under the same subscription name starts fresh at the topic's current position instead of resuming where it left off.
Both are irreversible for the messages involved. Nothing in Pulsar can un-acknowledge a skipped message or restore a deleted cursor, which is why the write sits behind auto_remediate, dry_run and a Pause. Four separate brakes sit on it:
grace_period_hours. A subscription seen for the first time in this run is recorded and left alone, so a rolling consumer restart never looks like an abandoned service.statsUpdateFrequencyInSecs, so every target is read again from the live per-topic stats endpoint immediately before the write. If a consumer reconnected in the meantime the target is skipped and nothing is written.target_backlog_messages. It also fails if the subscription still exists at all when the action was unsubscribe.GET /admin/v2/persistent/{tenant}/{namespace} lists the topics the namespace holds right now./admin/v2/broker-stats/topics returns every topic the broker owns with its subscriptions, each carrying msgBacklog and its connected consumer list.min_backlog_messages.max_subscriptions.dry_run true or auto_remediate false the run stops at a log of exactly what it would have called. That is the default.Pause waits for a human. On approval a Loop with concurrencyLimit: 1 walks the targets one at a time: live re-check, sample through io.kestra.plugin.pulsar.Reader, then POST .../subscription/{sub}/skip_all or DELETE .../subscription/{sub}.Storage you can actually reclaim. Once no subscription references a ledger, Pulsar's own retention and ledger trimming can delete it, so the broker's disk goes down instead of sideways. You also get a decision you can defend: the target list, the backlog each subscription held, the consumer counts the broker reported, the grace window each one passed, the sampled messages. It reports the live numbers from the re-read after the write.
The sampled messages land in Kestra internal storage as an ion file at {{ outputs.sample_doomed_messages.uri }}, one row per message, with {{ outputs.sample_doomed_messages.messagesCount }} as the count. The per-target before and after states are logged as JSON, for example {"present":true,"backlog":3074,"bytes":174569,"consumers":0} ahead of the write and {"present":true,"backlog":0,"bytes":0,"consumers":0} after it.
The age gate is the part that makes this safe to schedule. A backlog alert cannot tell a decommissioned exporter from a consumer fleet rolling through a deploy. Two sightings separated by the grace window can.
Platform and streaming teams running Pulsar for more than one product team, where subscriptions outlive the services that created them. If broker storage keeps climbing while message rates are flat, this is usually why.
The job is a sequence of admin calls with a decision in the middle, which is what a flow is good at. Kestra supplies the parts a cron script would have to grow for itself: a KV store for the first-seen clock that survives restarts, a Pause that puts a named human in front of an irreversible call, secrets for the admin token, a re-read that can fail the run. It keeps a log of every number the decision used. The Pulsar plugin reads from the same broker in the same flow, so the evidence and the remediation share one credential set and one audit trail.
Run against Apache Pulsar 3.3.1 in standalone mode on Kestra 2.0.5. One topic held 2000 messages with several subscriptions on it: orphaned ones with zero consumers and a backdated first-seen, one with a live consumer. One more was freshly created and consumerless inside the grace window.
msgBacklog 2000 with 0 consumers, left the subscription with a live consumer alone. It left the fresh orphan to the age gate.dry_run: true: the plan named the target and the admin API showed the backlog unchanged.auto_remediate: true with dry_run: false and approval: skip_all took the backlog from 2000 to 0 and the live re-read confirmed it. unsubscribe removed the subscription and the re-read confirmed it was gone.skip_all could not hold the backlog at the target, the live re-read caught it and the run failed instead of reporting success.errors block and posted to Slack.Every before and after number was read from the Pulsar admin API directly, not from the Kestra log.
A reachable Pulsar admin REST endpoint, an account with namespace-level admin rights on the tenant. The broker service URL is needed for the plugin read. The admin role needs skip_all and unsubscribe permission on the namespace.
PULSAR_ADMIN_TOKEN: bearer token for the Pulsar admin REST API. Used while auth_mode is token.SLACK_WEBHOOK_URL: incoming webhook for findings and failures.admin_url: base URL of the Pulsar admin REST API, for example http://pulsar-broker:8080.broker_uri: broker service URL for the plugin read, for example pulsar://pulsar-broker:6650.auth_mode: token to send the bearer token, none for a cluster with authentication disabled.tenant, pulsar_namespace: the namespace to sweep.min_backlog_messages: backlog at or above which a consumerless subscription is a candidate.grace_period_hours: how long a subscription must stay consumerless before it can be touched.target_backlog_messages: backlog the live re-read must show after a skip_all.remediation_action: skip_all keeps the subscription with an empty backlog, unsubscribe deletes it.max_subscriptions: cap on how many are acted on per run.sample_messages: how many messages to read as evidence before each write.dry_run, auto_remediate: the safety gates. Defaults are read-only.PULSAR_ADMIN_TOKEN and SLACK_WEBHOOK_URL to your namespace secrets.min_backlog_messages until the audit lines up with what you know about that namespace, then raise it to the level you want paged on.auto_remediate: true and dry_run: false and approve the pause by hand./admin/v2/broker-stats/topics reports the topics the broker you called owns. On a multi-broker cluster run the flow once per broker admin URL. The alternative is one run per namespace behind the proxy, looping the namespaces.lastConsumedTimestamp and lastAckedTimestamp. Fold either into the eligibility test for a second idle signal next to the KV first-seen clock.POST /admin/v2/namespaces/{tenant}/{ns}/backlogQuota so new subscriptions cannot repeat the growth. Keep this flow for the ones that already did.