An unbound push consumer is a JetStream consumer configured with a deliver_subject but no client currently subscribed to that subject. While unbound, messages are delivered to the deliver subject with no receiver — they accumulate in ack pending and trigger redeliveries until max_deliver is reached, wasting server resources and potentially losing messages.
Push consumers are fire-and-forget from the server’s perspective. When you create a push consumer, the server immediately starts delivering messages to the specified deliver_subject. If nothing is listening on that subject, the messages aren’t queued — they’re sent into the void. The server marks them as delivered, ack_wait expires with no acknowledgment, and the messages are redelivered. This cycle repeats up to max_deliver times before the messages are abandoned.
The resource cost is real. Each delivery attempt consumes server CPU for message lookup, serialization, and routing. The ack_wait timer for every unacknowledged message consumes memory. On a high-throughput stream, an unbound push consumer can generate thousands of pointless delivery attempts per second, creating load that serves no purpose. Multiply this by several orphaned push consumers — a common situation in environments where consumers are created programmatically — and the waste becomes significant.
The operational risk is worse than the resource cost. An unbound push consumer that was supposed to have an active subscriber means messages are not being processed. If that consumer handles order confirmations, inventory updates, or audit events, the gap in processing may not be noticed until the business impact surfaces — missing records, inconsistent state, compliance gaps. Unlike a pull consumer that simply waits for a client to fetch, a push consumer with no subscriber is actively losing data.
Subscriber application crashed or was stopped. The most common cause. The consumer was created, the subscriber connected and ran for a while, then the subscriber process was killed, OOM’d, or failed to restart after a deployment. The push consumer remains configured on the server, delivering to an empty subject.
Deployment timing gap. In orchestrated environments, the JetStream consumer is created by infrastructure automation (Terraform, Helm, startup scripts) before the subscribing application is running. There’s a window where messages are being delivered with no subscriber. If the subscriber never starts — a failed deployment, a misconfigured image — the window becomes permanent.
Deliver subject typo or mismatch. The consumer is configured to deliver to _INBOX.orders.deliver but the subscriber listens on _INBOX.order.deliver. A single character difference means the subscriber never receives messages. This is especially common with dynamically generated deliver subjects.
Legacy consumer left after architecture change. The system was redesigned to use pull consumers, but the old push consumer was never deleted. It sits on the stream, consuming redelivery cycles, while the new pull consumer handles the actual workload.
Ephemeral consumer with no idle heartbeat. Ephemeral push consumers are cleaned up when their subscriber disconnects — but only if idle heartbeats are configured. Without heartbeats, the server has no mechanism to detect that the subscriber is gone, and the consumer persists indefinitely.
Check all consumers on a stream to find push consumers with no active subscribers:
nats consumer report <stream_name>Push consumers show a deliver subject in the output. If the Ack Pending count is high and growing while Waiting (for pull consumers) or active subscriber count is zero, the consumer is likely unbound.
For detailed information on a specific consumer:
nats consumer info <stream_name> <consumer_name>Key fields:
If push_bound is false and num_ack_pending is non-zero, the consumer is unbound and cycling through redeliveries.
Verify whether anything is subscribed to the consumer’s deliver subject:
nats server report connections --subscriptionsSearch the output for the deliver subject. If no connection has a matching subscription, the push consumer is unbound.
To find all unbound push consumers across your entire JetStream deployment:
for stream in $(nats stream list --names); do echo "=== $stream ===" nats consumer report "$stream" 2>/dev/nulldoneLook for push consumers with high redelivery counts and no active subscriber. These are candidates for cleanup or conversion.
Delete orphaned push consumers that are no longer needed. If the subscriber application has been decommissioned or replaced:
nats consumer delete <stream_name> <consumer_name>Start the missing subscriber. If the push consumer is supposed to have an active subscriber, restart the subscribing application. Check orchestration logs (Kubernetes events, systemd status) to find out why it stopped.
Pull consumers are inherently safe from this problem — they don’t deliver messages unless a client explicitly fetches. For most workloads, converting from push to pull is the right long-term move:
1// Go client — pull consumer (recommended)2js, _ := nc.JetStream()3
4// Create a pull consumer instead of push5_, err := js.AddConsumer("ORDERS", &nats.ConsumerConfig{6 Durable: "order-processor",7 AckPolicy: nats.AckExplicitPolicy,8 AckWait: 60 * time.Second,9 MaxAckPending: 1000,10 FilterSubject: "ORDERS.>",11 // No DeliverSubject — this makes it a pull consumer12})13
14// Pull messages explicitly15sub, _ := js.PullSubscribe("ORDERS.>", "order-processor")16for {17 msgs, _ := sub.Fetch(100, nats.MaxWait(5*time.Second))18 for _, msg := range msgs {19 processOrder(msg)20 _ = msg.Ack()21 }22}1# Python (nats.py) — pull consumer2import nats3
4nc = await nats.connect()5js = nc.jetstream()6
7# Create pull consumer (no deliver_subject)8await js.add_consumer("ORDERS", nats.js.api.ConsumerConfig(9 durable_name="order-processor",10 ack_policy=nats.js.api.AckPolicy.EXPLICIT,11 ack_wait=60,12 max_ack_pending=1000,13 filter_subject="ORDERS.>",14))15
16# Fetch messages explicitly17sub = await js.pull_subscribe("ORDERS.>", "order-processor")18while True:19 msgs = await sub.fetch(100, timeout=5)20 for msg in msgs:21 await process_order(msg)22 await msg.ack()If push consumers are required, configure idle heartbeat and flow control. These allow the server to detect when the subscriber disconnects:
nats consumer add <stream_name> <consumer_name> \ --target="deliver.orders" \ --heartbeat=30s \ --flow-control \ --ack explicit \ --wait=60sTie consumer lifecycle to application lifecycle. Create consumers in application startup code, not infrastructure automation. Use ephemeral consumers with idle heartbeats for workloads that don’t need persistence across restarts:
1// Ephemeral push consumer — cleaned up when subscriber disconnects2_, err := js.Subscribe("EVENTS.>", handler,3 nats.IdleHeartbeat(30*time.Second),4 nats.EnableFlowControl(),5)Audit consumers regularly. Add a periodic check that identifies push consumers with no active subscriber. Synadia Insights performs this check automatically every collection epoch, flagging unbound push consumers before they accumulate significant redelivery waste.
Default to pull consumers for new workloads. Pull consumers have no unbound state — they simply wait for clients to fetch. Unless your architecture specifically requires server-initiated delivery (rare in modern designs), pull consumers are safer and more operationally predictable.
A push consumer has a deliver_subject configured — the server proactively sends messages to that subject. A pull consumer has no deliver subject — clients must explicitly fetch messages. The key operational difference: push consumers deliver whether or not anyone is listening, while pull consumers only deliver when asked. This makes pull consumers inherently safer against the unbound consumer problem.
Yes, effectively. The server delivers messages to the deliver subject, but with no subscriber, they’re not received. The server then waits for ack_wait to expire and redelivers them. This cycle repeats up to max_deliver times. Once max_deliver is exhausted, the message is skipped permanently. If max_deliver is not set, messages cycle indefinitely, wasting resources but never progressing.
Ephemeral consumers (no durable_name) with idle heartbeats are automatically deleted by the server when the subscriber disconnects and heartbeats stop. The server sends heartbeat messages to the deliver subject; if no response is received within the idle heartbeat interval, the consumer is removed. Without idle heartbeats configured, ephemeral consumers persist and become unbound.
No. You cannot change a consumer between push and pull in-place — deliver_subject is immutable after creation. The migration path is: note the current consumer’s delivered_stream_seq from nats consumer info, delete the push consumer, create a new pull consumer with opt_start_seq set to the last delivered sequence. This preserves position but requires a brief processing gap during the switch.
Use pull consumers instead — they eliminate the problem entirely. If push consumers are required, ensure the subscriber starts before the consumer is created (reverse the deployment order), or use ephemeral consumers with idle heartbeats that self-clean when the subscriber is absent. In Kubernetes, use init containers or readiness probes to ensure the subscriber is ready before creating the JetStream consumer.
With 100+ always-on audit Checks from the NATS experts, Insights helps you find and fix problems before they become costly incidents.
No alert rules to write. No dashboards to maintain.
News and content from across the community