Consumers are connected. Producers are publishing. Backlog looks flat or zero. But no messages are being processed.
Pulsar brokers enforce a per-subscription limit on unacknowledged messages (maxUnackedMessagesPerSubscription, default 200,000) and a per-consumer limit (maxUnackedMessagesPerConsumer, default 50,000). When unacked messages hit either ceiling, the broker stops dispatching new messages to that subscription or consumer. No error is returned to the client. No exception is thrown. Dispatch pauses silently.
Most monitoring focuses on backlog and connection count. Both can look healthy during a freeze. Messages were already dispatched to consumers, so backlog may read zero. Consumers remain connected at the TCP level. The definitive indicators are the unacked message count itself and the blockedSubscriptionOnUnackedMsgs flag in topic stats.
Mechanism
When a consumer receives a message, the broker increments the unacked counter for that subscription. When the consumer acknowledges, the counter decrements. If consumers process slower than the dispatch rate, or stop acknowledging entirely, the counter climbs. At the limit, the broker sets blockedSubscriptionOnUnackedMsgs to true and halts further dispatch.
The freeze persists until the unacked count drops below the limit. This happens when consumers acknowledge messages, when ack timeouts trigger redelivery (if configured), or when messages are otherwise cleared from the unacked set. If consumers are genuinely stuck and no ack timeout is configured, the freeze persists indefinitely.
flowchart TD
A[Consumer connected, receiving messages] --> B[Consumer stops or slows acknowledging]
B --> C[Unacked count rises toward limit]
C --> D{Unacked at limit?}
D -->|No| C
D -->|Yes| E[Broker sets blockedSubscriptionOnUnackedMsgs = true]
E --> F[Dispatch stops for this subscription]
F --> G[Consumers stay connected, no errors]
G --> H[Backlog may read stable or zero]
H --> I[Silent freeze: no forward progress]There is also a per-consumer limit. When maxUnackedMessagesPerConsumer (default 50,000) is exceeded for an individual consumer, the broker sets blockedConsumerOnUnackedMsgs to true for that consumer. In a shared subscription with multiple consumers, this can affect individual consumers independently.
Version-specific gotchas
Two upstream bugs complicate diagnosis on certain Pulsar versions:
- The blocked metric was unreliable before Pulsar 3.0.0. The
pulsar_subscription_blocked_on_unacked_messagesPrometheus metric could report 0 even when the subscription was actively blocked. If you are on 2.x, do not rely on this metric alone. Use the Admin API fieldblockedSubscriptionOnUnackedMsgsinstead. - A regression caused permanent consumer freeze on some 3.0.x through 4.0.x versions. Consumers could stop receiving messages even when
pulsar_subscription_unacked_messageswas 0 andpulsar_subscription_blocked_on_unacked_messageswas 0, because the per-consumerblockedConsumerOnUnackedMsgsflag got stuck attrueafter acknowledgements completed. If you see freeze symptoms but the subscription-level metrics look clean, check the per-consumer blocked flag in topic stats.
There is also a long-standing issue where pulsar_subscription_unacked_messages can report negative values due to batch acknowledgement counting problems. Monitor the absolute value or cross-check with the Admin API for authoritative counts.
Common causes
| Cause | What it looks like | First thing to check |
|---|---|---|
| Consumer processing too slow | Unacked count rising steadily, low ack rate | Consumer application logs for processing latency |
| Consumer bug: never acknowledges | Unacked rising linearly, ack rate zero | Consumer code path for acknowledge() calls |
| Downstream dependency failure | Unacked rising, consumer logs show DB or API timeouts | Consumer’s downstream service health |
| Poison message | High redelivery rate, unacked rising then plateauing | Consumer error logs for repeated failures on the same message |
| Ack timeout misconfigured | Unacked elevated, redelivery cycling without progress | Consumer ackTimeout and ackTimeoutTickDuration settings |
| Stuck blocked flag (bug) | Freeze symptoms but metrics show 0 unacked and not blocked | Per-consumer blockedConsumerOnUnackedMsgs in topic stats |
Quick checks
Run these read-only checks on the affected topic and subscription. None of them modify state.
# Check unacked message count per subscription from Prometheus metrics
curl -s http://<broker-host>:8080/metrics | grep pulsar_subscription_unacked_messages
# Check whether the subscription is flagged as blocked
curl -s http://<broker-host>:8080/metrics | grep pulsar_subscription_blocked_on_unacked_messages
# Get detailed subscription stats via Admin API
pulsar-admin topics stats persistent://tenant/namespace/topic
# Look for:
# subscriptions.<name>.unackedMessages
# subscriptions.<name>.blockedSubscriptionOnUnackedMsgs
# subscriptions.<name>.consumers[].blockedConsumerOnUnackedMsgs
# subscriptions.<name>.consumers[].availablePermits
# Check dispatch rate vs publish rate for the affected topic
curl -s http://<broker-host>:8080/metrics | grep -E "pulsar_rate_(in|out)"
# Check redelivery rate for the subscription
curl -s http://<broker-host>:8080/metrics | grep pulsar_subscription_msg_rate_redeliver
# Verify consumer connections are still active
curl -s http://<broker-host>:8080/admin/v2/persistent/tenant/namespace/topic/stats | \
jq '.subscriptions | to_entries[] | {sub: .key, consumers: (.value.consumers | length)}'
The availablePermits field in consumer stats deserves attention. A value of 0 means the client library’s internal receive queue is full and receive() is not being called. This is a separate mechanism from the unacked limit, but it produces the same symptom: the dispatcher stops sending messages. If availablePermits is 0 and blockedSubscriptionOnUnackedMsgs is false, the bottleneck is client-side, not broker-side.
How to diagnose it
Identify the affected subscription. Compare
pulsar_rate_inandpulsar_rate_outper topic. A topic with non-zero publish rate but zero dispatch rate is your target. If multiple subscriptions exist on the topic, check each independently.Check the unacked count. Pull
pulsar_subscription_unacked_messagesfor the subscription. Compare against your configuredmaxUnackedMessagesPerSubscription. If you have not changed the default, the limit is 200,000. Alerting at 50% of the limit (100,000 for the default) gives a leading indicator before the freeze triggers.Check the blocked flag. Query the Admin API for
blockedSubscriptionOnUnackedMsgson the subscription andblockedConsumerOnUnackedMsgson each consumer. The Admin API is more authoritative than the Prometheus metric, especially on versions before 3.0.0.Check
availablePermits. If the subscription is not blocked but dispatch has stopped, checkavailablePermitson each consumer. A value of 0 means the client is not requesting more messages. This points to a client-side issue, not a broker-side block.Check redelivery rate. High redelivery (
pulsar_subscription_msg_rate_redeliver) relative to dispatch rate indicates poison messages or processing failures. If every message is being redelivered, the consumer is receiving but failing to process, which means it will never ack and the unacked count will stay elevated.Check the backlog. If
pulsar_subscription_back_logis zero or stable whilepulsar_rate_outis zero, messages were dispatched but not acknowledged. This is the signature of the unacked freeze. A growing backlog with zero dispatch rate means no consumers are connected at all, which is a different problem.Rule out the version-specific freeze bug. If unacked is 0, the subscription is not blocked, dispatch is zero, and consumers are connected, check
blockedConsumerOnUnackedMsgsper consumer. If it is stuck attrue, you may be hitting the stuck-blocked-flag regression. The temporary workaround is to restart the affected consumer.
Metrics and signals to monitor
| Signal | Why it matters | Warning sign |
|---|---|---|
pulsar_subscription_unacked_messages | Count of dispatched but unacknowledged messages. Approaching the limit means dispatch freeze is imminent. | Sustained value above 50% of maxUnackedMessagesPerSubscription |
pulsar_subscription_blocked_on_unacked_messages | Binary flag indicating the subscription is dispatch-frozen. | Value of 1 (blocked) |
blockedSubscriptionOnUnackedMsgs (Admin API) | Authoritative blocked flag, more reliable than the Prometheus metric on older versions. | true |
blockedConsumerOnUnackedMsgs (Admin API, per consumer) | Per-consumer block flag. Can be stuck at true due to known bugs. | true while unacked is 0 |
availablePermits (per consumer) | Client-side flow control. 0 means the client is not pulling messages. | Any value that does not increase over time |
pulsar_subscription_msg_rate_redeliver | Messages being redelivered indicate processing failures. | Above 10% of dispatch rate |
pulsar_rate_out vs pulsar_rate_in | Dispatch rate vs publish rate. Divergence means backlog or freeze. | rate_out at zero with non-zero rate_in |
pulsar_subscription_back_log | Messages not yet dispatched. Can be zero during a freeze. | Zero or stable while dispatch is stopped (confirms freeze, not consumer absence) |
Fixes
Consumer is processing too slowly
The consumer receives messages but its processing pipeline cannot keep up with the dispatch rate.
- Scale out consumers. If the subscription type is Shared or Failover, adding consumer instances increases parallelism. For Key_Shared, ensure keys are well-distributed across consumers.
- Increase the unacked limit. Raising
maxUnackedMessagesPerSubscription(broker-level config) gives consumers more headroom. This is a bandage, not a fix. If consumers cannot keep up, a higher limit just delays the freeze. - Tune consumer receiver queue size. A larger receiver queue means more messages buffered client-side, but it also means more memory per consumer and more unacked messages in flight.
Consumer is not acknowledging at all
The consumer receives messages but never calls acknowledge(). This is a code defect.
- Review the consumer’s processing pipeline. Ensure
acknowledge()is called on every successfully processed message, including in error paths that handle and recover from failures. - Check for swallowed exceptions. If the consumer catches an exception, logs it, but does not nack or ack the message, the message stays unacked indefinitely (or until ack timeout triggers redelivery).
Downstream dependency failure
The consumer is blocked waiting on a slow database, API, or external service. Messages pile up in the unacked set because processing cannot complete.
- Fix the downstream dependency. This is the root cause.
- Set appropriate timeouts. If the consumer is stuck on a downstream call, it should time out and either nack the message or move on. Without a timeout, one hung downstream call can freeze the entire subscription.
- Configure
ackTimeouton the consumer. This causes the broker to redeliver messages that have been unacked for too long. WithoutackTimeoutconfigured (the default is no timeout), unacked messages stay unacked indefinitely.
Poison message
A specific message always fails processing. The consumer receives it, fails, and the message gets redelivered. Redelivery rate spikes.
- Configure a dead letter topic. After
maxRedeliveryCountredeliveries, the message routes to a DLQ and stops clogging the subscription. - Skip the message manually. If you identify the poison message by ID, you can acknowledge it explicitly to clear it from the unacked set. This loses the message, so coordinate with the application team.
- Fix the consumer. If the consumer should handle this message type, fix the deserialization or processing logic.
Stuck blocked flag (version bug)
On affected Pulsar versions, blockedConsumerOnUnackedMsgs can remain true after all messages are acknowledged. The consumer stops receiving permanently.
- Restart the affected consumer. This clears the stuck flag temporarily. The bug will recur.
- Upgrade. Upgrade to a patched version to resolve permanently.
Prevention
- Alert on unacked count at 50% of the limit. By the time you hit 100%, the freeze is already active and consumers are stuck. Alert when
pulsar_subscription_unacked_messagesexceeds 50% ofmaxUnackedMessagesPerSubscriptionfor more than 10 minutes. - Configure
ackTimeouton consumers. Without it, unacked messages never expire. A reasonableackTimeout(matching your expected processing time) ensures stuck messages eventually get redelivered rather than sitting in the unacked set forever. - Use dead letter topics. Poison messages cause repeated redelivery, consuming broker resources and keeping unacked counts elevated. A DLQ with an appropriate
maxRedeliveryCountprevents infinite retry loops. - Monitor the blocked flag directly. Even with unacked alerting, a separate alert on
pulsar_subscription_blocked_on_unacked_messages == 1(or the Admin API equivalent) provides a definitive “this is frozen right now” signal. - Track redelivery rate. High redelivery relative to dispatch rate is a leading indicator of poison messages and processing failures that will eventually push unacked counts up.
- Upgrade past known bugs. If you are on an affected version range, the stuck-blocked-flag regression is a live risk. Plan an upgrade to a patched release.
How Netdata helps
- Per-second resolution on
pulsar_subscription_unacked_messages. The difference between a consumer slowly falling behind and one that has hit the wall is visible in the slope of the unacked count. Per-second granularity makes that slope legible before the freeze triggers. - Correlate unacked count with dispatch rate. When
pulsar_rate_outdrops to zero whilepulsar_subscription_unacked_messagesis at the limit, the diagnosis is immediate. Netdata’s correlated dashboards put these signals on the same timeline without manual cross-referencing. - ML anomaly detection on unacked trends. A slowly climbing unacked count that has not yet crossed the 50% threshold is easy to miss in threshold-based alerting. Anomaly detection flags the deviation from baseline earlier.
- Alert at 50% of the limit. Configurable alerts on the unacked-to-limit ratio give you the leading indicator before the freeze happens, not after.
- Track the blocked flag as a dedicated signal. The
pulsar_subscription_blocked_on_unacked_messagesmetric is surfaced directly as a binary state change rather than requiring inference from rate drops.
Related guides
- Apache Pulsar active connections climbing: connection leaks and file descriptor exhaustion
- Apache Pulsar bookie add-entry queue not draining: writes arriving faster than the disk can commit
- Apache Pulsar AutoRecovery stalled: under-replicated ledgers that never heal
- Apache Pulsar bookie disk filling: runway to read-only and how to reclaim space
- Apache Pulsar bookie failure cascade: recovery I/O that topples surviving bookies
- Apache Pulsar bookie read latency high: catch-up reads competing with the write path
- Apache Pulsar bookie read-only: disk full and bookie_SERVER_STATUS at zero
- Apache Pulsar broker down: telling a dead broker from a fenced one
- Apache Pulsar broker GC death spiral: heap pressure, stop-the-world pauses, and lost topic ownership
- Apache Pulsar broker lookup failures: new clients cannot find their topic
- Apache Pulsar entry log GC falling behind: reclaimed space that never comes back
- How Apache Pulsar actually works in production: a mental model for operators






