rabbitmq debugging consumers operations

RabbitMQ Consumer Not Processing Messages: 6 Causes & Fixes

RabbitMQ consumer connected but not processing? Here are the 6 most common causes — prefetch, flow control, nack loops — and how to fix each one fast.

Qarote Team
6 min read

Consumer is connected. Queue has messages. Nothing moves. Every monitoring surface gives you a green light — the process is up, the AMQP connection is established, the management plugin shows a consumer on the queue. But your ack rate is zero.

There are exactly six things that cause this, and only one of them is actually about your consumer code. Here’s how to find yours — ranked by how often they appear in production.

Before running CLI commands: Qarote shows consumer count, unacked message count, utilisation, and DLQ depth on one screen in real time. Four of the six causes below are visible there without touching a terminal.

If your queue is also backing up while consumers are stuck, start with How to Debug a RabbitMQ Queue Backlog for the queue-side diagnosis, then come back here for the consumer side.


1. Prefetch count too low — RabbitMQ blocks message delivery

RabbitMQ’s AMQP basic.qos prefetch setting controls how many unacknowledged messages a consumer can hold at once. If your consumer has prefetch_count = 1 and is stuck on a slow operation, RabbitMQ won’t send the next message until the first one is acknowledged.

Confirm this is your problem first:

curl -u guest:guest http://localhost:15672/api/queues/%2F/my-queue \
  | jq '{messages_unacknowledged, messages_ready, consumer_utilisation}'

If messages_unacknowledged equals your prefetch_count exactly and messages_ready is nonzero, this is your issue. The queue has messages but RabbitMQ is refusing to deliver them because every in-flight slot is occupied.

Fix:

# Python (pika)
channel.basic_qos(prefetch_count=10)
// Node.js (amqplib)
await channel.prefetch(10);

Set prefetch to a multiple of your concurrent processing capacity. If each worker can handle 5 operations in parallel, prefetch_count = 5 lets it stay fully utilized.


2. Consumer connected but blocked on I/O

A connected consumer is not the same as a processing consumer. If your consumer is waiting on a database query, external HTTP call, or file operation that never returns, it’s alive but doing nothing.

Confirm: check if messages_unacknowledged is static (not changing) while the consumer process shows 0% CPU. A blocked consumer burns no CPU — it’s waiting on a syscall. A slow consumer burns CPU. This distinction is fast to check.

Look for these signs:

  • Unacked count equals prefetch count and doesn’t change over time
  • Consumer utilisation is 1.0 but throughput is near zero
  • Your app logs show a request that started but never logged completion

Common blocking operations by category:

  • Database: connection pool exhausted, waiting for a slot. Fix: add pool timeout, log pool wait time, or reduce prefetch to match pool size.
  • HTTP downstream: no timeout set. Fix: always set connect_timeout and read_timeout separately — connect_timeout=2, read_timeout=5 is a reasonable starting point.
import requests
# Never omit timeout — both connect and read
response = requests.get(url, connect_timeout=2, timeout=5)
  • Filesystem: writing to NFS or a slow mounted volume. Fix: move to async writes or a local volume.
  • Internal lock: your message handler holds a mutex another thread also holds. Fix: add lock acquisition timeout and log contention.

3. Nack loop: consumer rejecting and requeuing messages silently

If your consumer calls basic.nack or basic.reject with requeue=true, the message goes straight back to the front of the queue — and your consumer picks it up again immediately. If the failure is deterministic (a bad payload, a permanently unavailable dependency), this creates what’s commonly called a poison message loop.

Check the message rate panel: if deliver rate is high but ack rate is zero, you’re in a nack loop.

Fix: for non-transient failures, reject with requeue=false so the message is dead-lettered:

channel.basic_nack(delivery_tag=method.delivery_tag, requeue=False)

Warning: requeue=False without a configured x-dead-letter-exchange drops the message permanently. Before switching from requeue=True to requeue=False, verify your queue has a DLQ configured:

curl -u guest:guest http://localhost:15672/api/queues/%2F/my-queue \
  | jq '.arguments["x-dead-letter-exchange"]'

If the result is null, configure a DLQ first — or you will lose messages silently.

Then inspect your DLQ to understand why messages are being rejected.


4. Consumer’s connection is flow-controlled

RabbitMQ can apply channel-level flow control when memory usage gets high. In this state, the broker stops delivering messages even to existing consumers. The consumer process is running fine — it just isn’t receiving anything.

Note: flow control on a consumer connection is often caused by the consumer also publishing messages on the same connection (for example, emitting result events after processing). RabbitMQ throttles the entire connection, which blocks message delivery as a side effect. If your consumer is also a publisher, split them onto separate connections — flow control will clear immediately.

Check for flow control:

curl -u guest:guest http://localhost:15672/api/channels \
  | jq '.[] | select(.flow_blocked == true) | .name'

Or look for flow control entries in /var/log/rabbitmq/rabbit@<hostname>.log.

Fix: investigate memory pressure on the broker. Common causes: a queue accumulating millions of messages, a large individual message, or vm_memory_high_watermark set too low. See RabbitMQ Memory Alarm: How to Diagnose and Fix It for the full remediation steps.


5. AMQP channel closed silently — consumer stops without restarting

Some AMQP client libraries swallow channel errors rather than surfacing them. Your consumer process is running, but the underlying channel was closed after a protocol error (unroutable message, codec error, permission issue), and nothing reconnected.

If your network drops the TCP connection without a FIN/RST, RabbitMQ detects the dead connection only after the heartbeat timeout expires — typically 60 seconds — leaving the consumer process alive but disconnected.

Look for channel.close or connection.close events in the RabbitMQ logs:

tail -n 100 /var/log/rabbitmq/rabbit@$(hostname).log | grep -E "closing|channel|error"

Fix: implement a reconnect loop in your consumer. Listen for channel/connection close events and re-establish:

connection.on("error", (err) => {
  console.error("Connection error", err);
  setTimeout(connect, 5000);
});

Idempotency warning: naive reconnection without idempotency guarantees causes duplicate processing. If your consumer nacked a message but the connection dropped before the nack reached the broker, RabbitMQ will redeliver it. Your handler must be idempotent — processing the same message twice must produce the same result as processing it once. If it’s not, track processed message IDs (in Redis, Postgres, etc.) before implementing auto-reconnect.


6. Wrong queue binding or queue name mismatch

This sounds obvious but happens more than you’d think, especially in staging environments: the publisher is routing to orders.created but the consumer subscribed to order.created (missing the s). Both queues exist, one is growing, the consumer is idle.

Verify bindings programmatically:

curl -u guest:guest \
  http://localhost:15672/api/bindings/%2F/e/my-exchange/q/my-queue \
  | jq '.[].routing_key'

Common topic exchange mismatches:

  • orders.# does NOT match orders — topic patterns require at least one word after the dot
  • #.created vs order.created.v2 — trailing version suffix breaks the pattern
  • Bindings are vhost-scoped — a consumer on vhost /staging won’t see messages in vhost /

The fastest way to debug topic routing without trial and error: use the Bindings tab in the management plugin for the specific exchange, then use the Publish message tool to test routing before changing application code.


What you can see without running a single CLI command

Four of the six causes above are directly visible in Qarote’s queue and consumer screens: whether consumers are connected (cause 1), unacked message count vs. prefetch limit (cause 2), consumer utilisation percentage (causes 2 and 4), and DLQ depth showing whether messages are being rejected (cause 3). Causes 5 and 6 — silent channel close and wrong binding — require logs and the management plugin. That split is the fastest way to triage: if the broker metrics look clean, the problem is in your application or configuration.

Qarote shows all of this on one screen in real time. The free Community edition covers all of these metrics — no per-host pricing, no data leaving your network.

Set this up once and stop debugging blind. Consumer utilisation dropping, unacked messages climbing, DLQ growing — all of these are catchable before they become incidents. How to set up RabbitMQ alerts that actually fire at the right time →


Decision tree

Consumer connected?
  No  → Restart consumer, verify re-subscription on the correct queue and vhost
  Yes → messages_unacknowledged = prefetch_count exactly?
        Yes → Prefetch too low or blocked on I/O (causes 1–2)
        No  → Ack rate > 0?
              No  → flow_blocked = true on connection?
                    Yes → Memory pressure on broker (cause 4)
                    No  → Check logs for channel.close events (cause 5)
              Yes → Processing but rejected?
                    Yes → Nack loop / poison message (cause 3)
                    No  → Check queue name and binding key (cause 6)

Tired of debugging RabbitMQ blind?

Qarote gives you a real-time view of queues, consumers, and alarms — free.

Get started free