Repository navigation
AbortSignal.any memory leak when circular dependencies #57584
Description
Activity
- addedabortcontrollerIssues and PRs related to the AbortController and AbortSignal APIs.Issues and PRs related to the AbortController and AbortSignal APIs.
on Mar 26, 2025 @jazelly and myself have spent some time digging into this issue.
We found that when a new signal is created using
AbortSignal.anyit will internally create a weak refnode/lib/internal/abort_controller.js
Line 262 in e57841f
const resultSignalWeakRef = new SafeWeakRef(resultSignal); When you attempt to hold a reference by
console.log(test)
It would prevent each other from being collected.
Couldn't say this is a bug but happy to hear your thoughts and discuss further.
Couldn't say this is a bug
I fail to understand how it couldn't be considered as a bug 🤔 I had a big memory leak in my app, spent hours investigating the issue, found this, had to workaround it.
It's completely unpredictable and leads to a memory leak
Reacted by Alexey ShThis is not a bug?
In your example, you never actually abort the signal, why would it get gc'd?
I'm not sure to understand what the gc has to do with aborting the signal 🤔 aborting a signal is an exception, not the rule
My point is: I have a legit function, that doesn't assign anything globally, and just because it needs to be abortable, there is no way to prevent it from leaking every time it's called.
Do you think there is anything wrong with the code I've provided? because it was making my app go OOM in less than half a day, with nothing obvious to me about what's wrong
Reacted by Pablo Sáez and Gürgün DayıoğluOh I see the problem, sorry, I was already working on an issue about
AbortSignal.anyand thought this was linked to it 😆So yeah I think this is being caused by
anyadding the resulting signal togcPersistentSignals, and it can't detect that it's out of scopeI am interested to learn what your work around is if you're willing to share of course.
What my previous comment was trying to say that nodejs internally uses
weakRefto hold a reference of the signal and when you have a circular dep (at least the V8 engine) it would give up on gc, you can reproduce it with just a WeakRef and circular deps without usingAbortSignal. Unlike other circular deps (that does not evloveWeakRefthat V8 can detect and reclaim the memory.I am also interested to see how other runtimes are dealing with this issue (e.g. deno, bun, chrome etc.), if they all have this memory issue, perhaps we should seek some help from the whatwg.
at the end, my workaround is to use a simpler custom version of the AbortSignal.any:
function abortSignalAny(signals: AbortSignal[]): AbortSignal { return { get aborted() { return signals.some((s) => s.aborted) }, addEventListener(type: 'abort', callback, options) { for (const signal of signals) { signal.addEventListener(type, callback, options) } }, removeEventListener(type: 'abort', callback, options) { for (const signal of signals) { signal.removeEventListener(type, callback, options) } }, dispatchEvent() { throw new Error('Unsupported') }, onabort() { throw new Error('Unsupported') }, get reason() { return signals.find((s) => s.reason != null)?.reason }, throwIfAborted() { for (const signal of signals) { signal.throwIfAborted() } } } }
There are some downsides, which are not really an issue for me though:
- signal.reason (and throwIfAborted) returns the reason of the first signal in the list, instead of the first triggered signal
- probably others?
Does it mean that removing abort stuff from an app would fix unexpected memory growing?
YES
github-actions commented
on Apr 20, 2026 on Apr 20, 2026 – with GitHub ActionsContributorMore actionsThis issue has been marked as stale due to 210 days of inactivity.
It will be automatically closed in 30 days if no further activity occurs. If this is still relevant, please leave a comment or update it to keep it open.- addedstaleIssues and PRs marked stale due to inactivity and scheduled for automatic closure.Issues and PRs marked stale due to inactivity and scheduled for automatic closure.
on Apr 20, 2026 - added a commit that references this issue
on Apr 23, 2026 github-actions commented
on May 21, 2026 on May 21, 2026 – with GitHub ActionsContributorMore actionsThis issue has been automatically closed after 30 days of inactivity following its stale status (no activity for a total of 240 days).
If this is still relevant, feel free to reopen it or leave a comment with additional details so we can continue the discussion.All the details are already there 🤔
- removedstaleIssues and PRs marked stale due to inactivity and scheduled for automatic closure.Issues and PRs marked stale due to inactivity and scheduled for automatic closure.
on May 25, 2026 github-actions commented
on Aug 24, 2026 on Aug 24, 2026 – with GitHub ActionsContributorMore actionsThis issue has been marked as stale due to 90 days of inactivity.
It will be automatically closed in 30 days if no further activity occurs. If this is still relevant, please leave a comment or update it to keep it open.- addedstaleIssues and PRs marked stale due to inactivity and scheduled for automatic closure.Issues and PRs marked stale due to inactivity and scheduled for automatic closure.
on Aug 24, 2026 No reason to think it's not relevant anymore 🤷
- removedstaleIssues and PRs marked stale due to inactivity and scheduled for automatic closure.Issues and PRs marked stale due to inactivity and scheduled for automatic closure.
on Aug 25, 2026
Version
23.10.0
Platform
Subsystem
No response
What steps will reproduce the bug?
run with
node --expose-gc test.tsHow often does it reproduce? Is there a required condition?
Every time
What is the expected behavior? Why is that the expected behavior?
the output should be
because the test object is not referenced anymore
What do you see instead?
output:
the test object is not garbage collected
Additional information
The issues seems to be caused by the cyclic dependency:
testobject =>abortControllerfield =>abortSignal=>AbortSignal.anyresult =>cbfunction =>testobjectAbortSignal.any seems to prevent node from garbage collecting them all at once