Skip to content

fix(microservice): avoid rmq pingcheck leaving queues behind - #2736

Open
GiHoon1123 wants to merge 1 commit into
nestjs:masterfrom
GiHoon1123:fix-issue-2680-rmq-healthcheck-no-queue-leak
Open

fix(microservice): avoid rmq pingcheck leaving queues behind#2736
GiHoon1123 wants to merge 1 commit into
nestjs:masterfrom
GiHoon1123:fix-issue-2680-rmq-healthcheck-no-queue-leak

Conversation

@GiHoon1123

@GiHoon1123 GiHoon1123 commented Jul 10, 2026

Copy link
Copy Markdown

PR Checklist

Please check if your PR fulfills the following requirements:

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Other... Please describe:

What is the current behavior?

Issue Number: #2680

MicroserviceHealthIndicator.pingCheck with Transport.RMQ and no queue specified creates a new RabbitMQ connection and calls assertQueue with whatever default queue name/options @nestjs/microservices' RMQ client falls back to. Depending on the version, that default queue name can be an empty string, which tells RabbitMQ to generate a fresh server-side queue name on every call. Verified this directly against a real broker: calling channel.assertQueue('', {}) three times produced three distinct queues (amq.gen-3T8sSiwFKQdPlXlClsU9gQ, amq.gen-uAM4xsfWRz42cY6qBmG7LQ, amq.gen-D5OdeGwtldapdHgI8p0I5A), each durable: true, auto_delete: false, exclusive: false - i.e. they persist in RabbitMQ forever once the health-check connection closes. Since a health check reconnects on every pingCheck call, every health check tick leaves one more orphaned durable queue behind, eventually overloading RabbitMQ.

What is the new behavior?

pingCheck now defaults noAssert: true for RMQ options when the caller hasn't specified queue, noAssert, queueOptions, exchange, or routingKey themselves. A plain connectivity check has no reason to declare a real queue at all, so this skips the assertQueue call entirely for the common case. If any of those options are explicitly set, that's treated as an opt-out and the caller's configuration is used as-is, unchanged.

Verification

What I did and didn't verify:

  • The RabbitMQ-side mechanism (empty queue name + default options → durable server-generated queue that outlives the connection) - verified directly with amqplib against a real broker, as described above.
  • The fix itself - verified with a real RabbitMQ instance: ran the existing RMQ > should connect e2e test (uses default options, no queue specified) with this fix applied, then checked RabbitMQ's management API (/api/queues) afterward - empty, no queues left behind.
  • The option-merging logic (respecting explicit queue/noAssert/queueOptions/exchange/routingKey, and leaving Kafka's existing defaults untouched) - covered by 6 new unit tests.
  • What I didn't do: a single-environment before/after comparison against the exact @nestjs/microservices version the issue was filed against. The version installed in this repo is 11.0.11, whose default RMQ queue name is still the literal string 'default' (asserting the same name repeatedly is idempotent, so it doesn't reproduce the queue accumulation on its own) rather than the empty-string default from the later version referenced in the issue. Verified the failure mechanism and the fix's effect independently instead of chaining them through the exact regressed dependency version in one run.

Full test suite (8 files, 77 tests) and lint pass.

Does this PR introduce a breaking change?

  • Yes
  • No

The only behavior change is for RMQ health checks that don't already specify queue/noAssert/queueOptions/exchange/routingKey - previously undefined/leaky behavior, not a documented contract.

Other information

RabbitMQ's client-rmq transport asserts a queue on every connect()
unless noAssert is set, using RQM_DEFAULT_QUEUE / RQM_DEFAULT_QUEUE_OPTIONS
as the default when the caller doesn't specify a queue. Some versions
of @nestjs/microservices default that queue name to an empty string,
which tells RabbitMQ to generate a fresh server-side queue name on
every call - verified directly against a real broker: calling
channel.assertQueue('', {}) three times produces three distinct
amq.gen-* queues, each durable and not auto-deleted. Since a
health-check client reconnects on every pingCheck, this means every
health check run leaves one more orphaned durable queue behind
forever.

Set noAssert: true by default when the caller passes RMQ options
without specifying queue, noAssert, queueOptions, exchange or
routingKey themselves - a plain connectivity check has no reason to
declare a real queue at all. Any of those options being explicitly set
is treated as an opt-out, so this only changes the previously-undefined
default case.

Verified with a real RabbitMQ instance: after fixing, running the
existing 'RMQ should connect' e2e test and checking RabbitMQ's
management API afterward shows no queues left behind. Note: the
locally installed @nestjs/microservices is 11.0.11, whose default
queue name is still the literal string 'default' (idempotent to
reassert), not the empty-string default from later versions the
issue was filed against - so this fixes the mechanism directly
(confirmed against a real broker) without also reproducing
before/after against the exact regressed dependency version in one
environment.

Fixes: nestjs#2680
@GiHoon1123

Copy link
Copy Markdown
Author

Just checking in on this one when you have a chance. No rush.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant