Troubleshooting¶
Symptom → likely cause → fix. If you don't see your problem here, check
docs/guide/full-guide.md for the feature you're
using, or open an issue.
Connection issues¶
"Connection refused" / can't connect at all.
Check ConnectionConfig.host/port/vhost against the actual broker.
Remember RabbitMQ's default management UI port (15672) is different from
the AMQP port (5672) — a common copy-paste mistake. If you're inside
Docker Compose or Kubernetes, confirm you're using the service name, not
localhost.
Connection appears healthy but publishes silently stall.
The connection is likely blocked by a RabbitMQ memory or disk alarm —
this is a real, distinct state from "disconnected." Check
broker_health_check(broker).blocked. Set
ConnectionConfig(blocked_connection_timeout=...) so a blocked connection
fails fast instead of stalling for minutes. See
Health Checks.
A publish-only SyncBroker keeps needing to reconnect.
Nothing is driving the connection's I/O loop between publishes — see
the sync-vs-async connection model.
Call broker.pump_idle() periodically from your own idle loop.
Messages stuck in retry / not reaching the DLQ¶
Messages seem to retry forever, or the delay queues never receive anything.
Confirm retry= is actually set (broker-wide or per-route) — this is what
declares the delay-queue topology and installs the retry middleware
together. If you manually added a RetryMiddleware to middlewares=[...]
without also setting retry= on the same route, the topology was never
declared and the middleware's delay-queue publishes are targeting
non-existent queues (you'll get a loud warning for this specific
misconfiguration).
A message is redelivered indefinitely and never dead-letters.
Check whether it's being reclassified as TRANSIENT on every attempt after
already exhausting max_retries — this should not happen (exhausted
retries dead-letter, they don't get re-retried), but if you've overridden
error classification with a custom unknown_policy or predicate, verify it
doesn't override the exhausted-attempt path. For a broker-enforced backstop
independent of any application logic, use a quorum queue with
delivery_limit=.
The DLQ is growing and I don't know why.
Use DLQInspector.peek("queue.dlq", limit=10) to look without consuming.
Check the message's x-death header (standard RabbitMQ dead-letter
metadata) for the original queue and reason. If it's a filter_fn
rejection, the auto-declared <queue>.dlq and its RuntimeWarning at
startup will tell you it exists — that's expected, not a leak, but you
should still be draining/alerting on it.
Consumer not receiving messages¶
Handler never fires, no errors.
Check the routing key / binding actually matches what's being published —
this is usually a topic-exchange wildcard mismatch (orders.* doesn't match
orders.created.eu, orders.# does). Verify with
rabbitkit topology list myapp.main:broker (requires rabbitkit[cli]) or
the RabbitMQ management UI's bindings view.
consumer_count is 0 in a health check even though the process is running.
The consumer channel likely died without the connection itself closing
(e.g. a channel-level protocol error). broker_readiness() cross-checks
consumer registration against actual channel liveness for exactly this
case — if it reports not-ready, trust it over "the process looks fine."
Idle / heartbeat disconnects¶
A low-traffic consumer gets marked "dead" by liveness checks, or
reconnects unexpectedly during quiet periods.
This was a real gap fixed in liveness heartbeat handling: the heartbeat is
now driven by the I/O loop tick (sync: once per start_consuming()
iteration; async: a periodic background task), not just message delivery —
so a healthy, quiet consumer stays "alive." If you're on an older version
or still seeing this, check wedged_timeout on broker_liveness() is set
generously relative to your actual traffic pattern.
A publish-only broker's connection drops after being idle.
Expected without pump_idle() — see the connection issues section above.
The consumer channel closes mid-handler with "delivery acknowledgement
… timed out" (PRECONDITION_FAILED).
RabbitMQ force-closes a channel if a delivered message stays unacked past
the server's consumer_timeout (default 30 minutes) — the classic
silent killer for long-running handlers with AckPolicy.MANUAL or a
generous/disabled TimeoutMiddleware. The server does not advertise
this limit to clients, so rabbitkit can't warn you up front. If a handler
can legitimately hold a message longer, raise the limit per queue at
declaration time — RabbitQueue(name=..., consumer_timeout=3_600_000)
(ms; RabbitMQ ≥ 3.12) — or ack earlier and track completion elsewhere.
Publish fails with MessageTooLargeError (a ValueError subclass):
Message body … exceeds PublisherConfig.max_message_bytes.
Working as intended: the client-side guard defaults to 16 MiB,
mirroring RabbitMQ's own max_message_size default. Without the guard the
server rejects the message anyway — but with a channel exception that kills
the (pooled) publisher channel and corrupts sibling in-flight publishes.
If you deliberately raised max_message_size in rabbitmq.conf, set
PublisherConfig(max_message_bytes=...) to match (the server doesn't
advertise its limit, so rabbitkit can't discover it); 0 disables the
guard. Better: store large payloads externally and publish a reference.
Installation¶
ModuleNotFoundError: No module named 'pkg_resources' when importing
aio-pika (or rabbitkit's async transport).
aio-pika==9.0.0 specifically imports pkg_resources, which recent
setuptools releases (>=81) no longer ship. This is an aio-pika packaging
issue, not a rabbitkit one — rabbitkit requires aio-pika>=9.1.0 precisely
because of this; 9.1.0 and later don't have the problem. If you've pinned
aio-pika to exactly 9.0.0 yourself (or an old lockfile resolved to it),
bump it to 9.1.0 or later.
Testing¶
A bug reproduces against a real broker but not TestBroker.
This is a known limitation, not a bug in TestBroker — it's an in-memory
fake that never speaks real AMQP, so it can't catch a bug in the transport
layer or real RabbitMQ topology (delay-queue TTL/DLX wiring, quorum-queue
delivery limits, real publisher confirms). Reproduce it in
tests/integration/ against a real broker (via testcontainers — no
manually-managed broker required, see
Real-broker integration tests).
Assertions on TestBroker.assert_acked()/assert_nacked() don't match
what I expect.
Remember TestBroker's settlement is real, not mocked — if your handler
raises and your AckPolicy is AUTO, the message is genuinely nacked or
rejected per the same classification logic production uses. If the
assertion is surprising, the classification (not the assertion) is usually
the thing to check first.
Security / signing¶
InvalidSignatureError on messages that look correctly signed.
Check content_encoding, routing_key, exchange, and reply_to — the
default signature covers all of these, not just the body. Republishing a
captured message under a different routing key, or flipping compression on
after signing, invalidates the signature by design.
Replay protection doesn't seem to work across multiple workers/pods.
Expected with the default nonce cache — it's per-process. Wire
nonce_cache=RedisNonceCache(redis.Redis(...)) to share the seen-set. You
should already be seeing a RuntimeWarning about this if you haven't.
Still stuck?¶
Check docs/guide/full-guide.md for the feature in
depth, docs/stability-policy.md to confirm you're
using an API in the tier you think it's in, or open an issue with a minimal
reproduction.