You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Service Bus] receiveMessages leaks an abort listener on every drain, causing unbounded memory growth #40407
Package Version: 7.9.5 (also present in 7.10.0 and on main)
Operating system: Linux (Kubernetes, node:22)
nodejs — version: 22
Describe the bug BatchingReceiverLite.tryDrainReceiver adds an "abort" listener to the caller's abortSignal, but only removes it on the abort and timeout paths. When the receiver drains normally, drainListener clears the timer and resolves without calling removeListeners(), so the abort listener stays attached.
A drain happens after most receiveMessages calls that end with leftover credit (for example every empty poll). A long-running consumer that passes the same shutdown AbortSignal to every receiveMessages call gains one listener per poll. In our service (17 receivers polling with maxWaitTimeInMs: 1000) that is ~60k listeners/hour. Node logs MaxListenersExceededWarning: ... abort listeners added to [AbortSignal], and the process eventually dies with JavaScript heap out of memory.
To Reproduce
Against the Service Bus emulator with an empty queue:
Summary:BatchingReceiverLite.tryDrainReceiver fails to remove its abort listener on the normal (non-abort, non-timeout) drain completion path, causing abort listeners to accumulate on long-lived AbortSignals and eventually leading to memory growth / MaxListenersExceededWarning.
📋 Issue Details
Package:@azure/service-bus 7.9.5 (also reproduced on 7.10.0 and main) Affected API:BatchingReceiverLite.tryDrainReceiver in sdk/servicebus/service-bus/src/core/batchingReceiver.ts Scenarios:
A long-running consumer passes the same shutdown AbortSignal to every receiveMessages call.
Every call that ends in a drain with leftover credit (e.g., an empty poll with maxWaitTimeInMs set) adds a new abort listener that is never removed.
At scale (e.g., 17 receivers polling every second), ~60k listeners/hour accumulate, producing MaxListenersExceededWarning and eventual JavaScript heap out of memory.
Root ask: Fix the drain-completion path so removeListeners() (which detaches both the abort listener and the receiverDrained listener) is invoked whenever drainListener fires, not only on abort or timeout.
🔎 Debugging / Reproduction Notes
Reviewed sdk/servicebus/service-bus/src/core/batchingReceiver.ts directly. Confirmed the code structure matches the report:
drainListener() clears the drain timer and resolves the promise, but does not call removeListeners().
onAbort() and the setTimeout timeout callback both correctly call removeListeners().
receiver.once(...) means the receiverDrained listener itself is auto-removed after firing, but the abortSignal.addEventListener("abort", onAbort) registration is not bound with { once: true }, so it persists indefinitely unless explicitly removed.
This means every normal drain (the common/majority case for empty-queue polling) leaks one abort listener per call.
Suggested investigation steps:
Add removeListeners() (or equivalently remove just the abort listener) inside drainListener before/after resolve().
Consider registering onAbort with { once: true } as defense-in-depth, though explicit cleanup is still needed since Node only auto-removes the listener it fired, not ones added via separate signals.
Add a regression test using getEventListeners(signal, "abort") (as in the repro) asserting the count returns to 0 after repeated receiveMessages calls that drain normally.
Check other call sites using checkAndRegisterWithAbortSignal in the same file (around the abortSignalCleanupFunction usage) for similar listener-leak patterns, since this is a recurring hygiene concern around AbortSignal usage in this file.
No similar open issues were found via search for "abort listener" or "MaxListenersExceededWarning" in this repository.
🏷️ Label Confidence
Category:Client — The affected code is client library source under sdk/servicebus/service-bus/src, not a management-plane (@azure/arm-*) package.
Service:Service Bus — Issue explicitly names @azure/service-bus and a sdk/servicebus source path.
Confidence: High — Package and affected API are explicitly identified, there is no ambiguity between services, and the category is clearly a client-library defect.
👥 Owner Routing
Matched CODEOWNERS entry:# ServiceLabel: %Service Bus — only entry in CODEOWNERS associating labels with Service Bus; no multi-label override exists for it.
Routing action: Added Service Attention and needs-team-attention labels; issue left unassigned; ServiceOwners mentioned in routing comment.
Scan notes: No other # ServiceLabel: entries reference "Service Bus" in combination with other labels, so the single-label entry was used directly without needing to scan further for a more specific override.
Generated by Agentic Triage for #40407 · copilot · auto · 57.5 AIC · ⌖ 9.71 AIC · ⊞ 15.9K · ◷
Activity
github-actions commentedon Oct 20, 2026
Thank you for your feedback. Tagging and routing to the team member(s) best able to assist.
//cc: Eldert Grootenboer (@EldertGrootenboer) Johnathan W (@j7nw4r) Josue Gomez (@SwayGom) Sagar Patel (@sagar0207) Srini Karri (@skarri-microsoft)
github-actions commentedon Oct 20, 2026
🎯 Agentic Issue Triage
Summary:
BatchingReceiverLite.tryDrainReceiverfails to remove itsabortlistener on the normal (non-abort, non-timeout) drain completion path, causing abort listeners to accumulate on long-livedAbortSignals and eventually leading to memory growth /MaxListenersExceededWarning.📋 Issue Details
Package:
@azure/service-bus7.9.5 (also reproduced on 7.10.0 andmain)Affected API:
BatchingReceiverLite.tryDrainReceiverinsdk/servicebus/service-bus/src/core/batchingReceiver.tsScenarios:
AbortSignalto everyreceiveMessagescall.maxWaitTimeInMsset) adds a newabortlistener that is never removed.MaxListenersExceededWarningand eventualJavaScript heap out of memory.Root ask: Fix the drain-completion path so
removeListeners()(which detaches both theabortlistener and thereceiverDrainedlistener) is invoked wheneverdrainListenerfires, not only on abort or timeout.🔎 Debugging / Reproduction Notes
Reviewed
sdk/servicebus/service-bus/src/core/batchingReceiver.tsdirectly. Confirmed the code structure matches the report:drainListener()clears the drain timer and resolves the promise, but does not callremoveListeners().onAbort()and thesetTimeouttimeout callback both correctly callremoveListeners().receiver.once(...)means thereceiverDrainedlistener itself is auto-removed after firing, but theabortSignal.addEventListener("abort", onAbort)registration is not bound with{ once: true }, so it persists indefinitely unless explicitly removed.abortlistener per call.Suggested investigation steps:
removeListeners()(or equivalently remove just the abort listener) insidedrainListenerbefore/afterresolve().onAbortwith{ once: true }as defense-in-depth, though explicit cleanup is still needed since Node only auto-removes the listener it fired, not ones added via separate signals.getEventListeners(signal, "abort")(as in the repro) asserting the count returns to 0 after repeatedreceiveMessagescalls that drain normally.checkAndRegisterWithAbortSignalin the same file (around theabortSignalCleanupFunctionusage) for similar listener-leak patterns, since this is a recurring hygiene concern aroundAbortSignalusage in this file.No similar open issues were found via search for "abort listener" or "MaxListenersExceededWarning" in this repository.
🏷️ Label Confidence
Client— The affected code is client library source undersdk/servicebus/service-bus/src, not a management-plane (@azure/arm-*) package.Service Bus— Issue explicitly names@azure/service-busand asdk/servicebussource path.👥 Owner Routing
# ServiceLabel: %Service Bus— only entry in CODEOWNERS associating labels with Service Bus; no multi-label override exists for it.@EldertGrootenboer,@j7nw4r,@SwayGom,@sagar0207,@skarri-microsoftService Attentionandneeds-team-attentionlabels; issue left unassigned; ServiceOwners mentioned in routing comment.# ServiceLabel:entries reference "Service Bus" in combination with other labels, so the single-label entry was used directly without needing to scan further for a more specific override.