Repository navigation
AbortController/AbortSignal memory leak #55328
Copy link
Copy link
Closed
Labels
confirmed-bugIssues and PRs for confirmed bugs.Issues and PRs for confirmed bugs.
Description
Bug Description
The memory of the Node process grows infinitely, ultimately resulting in an OOM error. Memory profiling revealed a memory leak related to AbortController.
Similar to #54614,but the difference is that if AbortSignal.any does not have an event listener added, AbortSignal.any will eventually be released and there will be no memory leak.
I am using fetch API with AbortController/AbortSignal in Node.js(not undici directly).
Sorry, I misremembered, I was using node:http+signal.
Reproducible By
const formatMemoryUsage = (data) =>
`${Math.round((data / 1024 / 1024) * 100) / 100} MB`;
// let i = 0;
// const registry = new FinalizationRegistry((heldValue) => {
// i += 1;
// // console.debug("[registry]", heldValue);
// });
let memoryData = process.memoryUsage();
console.log("Mem before loop", formatMemoryUsage(memoryData.rss));
const run = () => {
for (let i = 0; i < 100_000; i++) {
const abortController = new AbortController();
const signal = abortController.signal;
const composedSignal = AbortSignal.any([signal]);
composedSignal.addEventListener("abort", () => {
// It looks like the closure reference in the callback function causes the signal to not be released?
// console.log(signal); // Uncommenting this will aggravate the memory leak
});
// registry.register(signal, "signal");
}
};
setImmediate(run);
setInterval(() => {
if (typeof global.gc == "function") {
global.gc(true);
} else {
console.warn("global.gc unavailable, need '--expose-gc' flag");
}
const memoryData = process.memoryUsage();
console.log("Mem after 100K iteration", formatMemoryUsage(memoryData.rss));
// console.log(i, "signals released");
}, 1000);/app # node --trace-gc --expose-gc test3.js
Mem before loop 43.78 MB
[380:0x7f3bd3df6000] 44 ms: Scavenge 3.9 (5.2) -> 3.5 (6.2) MB, pooled: 0 MB, 0.74 / 0.00 ms (average mu = 1.000, current mu = 1.000) allocation failure;
[380:0x7f3bd3df6000] 45 ms: Scavenge 3.9 (6.2) -> 3.8 (6.7) MB, pooled: 0 MB, 0.62 / 0.00 ms (average mu = 1.000, current mu = 1.000) allocation failure;
[380:0x7f3bd3df6000] 47 ms: Scavenge 4.5 (6.7) -> 4.3 (9.2) MB, pooled: 0 MB, 0.74 / 0.00 ms (average mu = 1.000, current mu = 1.000) allocation failure;
[380:0x7f3bd3df6000] 50 ms: Scavenge 5.9 (9.2) -> 5.4 (9.7) MB, pooled: 0 MB, 0.99 / 0.00 ms (average mu = 1.000, current mu = 1.000) allocation failure;
[380:0x7f3bd3df6000] 53 ms: Scavenge 6.3 (9.7) -> 6.0 (14.7) MB, pooled: 0 MB, 1.20 / 0.00 ms (average mu = 1.000, current mu = 1.000) allocation failure;
[380:0x7f3bd3df6000] 57 ms: Scavenge 9.5 (14.8) -> 8.4 (15.6) MB, pooled: 0 MB, 1.41 / 0.00 ms (average mu = 1.000, current mu = 1.000) allocation failure;
[380:0x7f3bd3df6000] 65 ms: Scavenge 10.6 (16.1) -> 10.0 (26.1) MB, pooled: 0 MB, 3.05 / 0.00 ms (average mu = 1.000, current mu = 1.000) allocation failure;
[380:0x7f3bd3df6000] 80 ms: Scavenge 16.8 (26.1) -> 15.2 (27.6) MB, pooled: 0 MB, 2.62 / 0.00 ms (average mu = 1.000, current mu = 1.000) allocation failure;
[380:0x7f3bd3df6000] 88 ms: Scavenge 18.9 (28.5) -> 18.3 (49.8) MB, pooled: 0 MB, 3.92 / 0.00 ms (average mu = 1.000, current mu = 1.000) allocation failure;
[380:0x7f3bd3df6000] 104 ms: Scavenge 33.7 (51.7) -> 31.0 (53.7) MB, pooled: 0 MB, 4.48 / 0.00 ms (average mu = 1.000, current mu = 1.000) allocation failure;
[380:0x7f3bd3df6000] 115 ms: Scavenge 35.8 (53.7) -> 34.8 (64.9) MB, pooled: 0 MB, 7.48 / 0.00 ms (average mu = 1.000, current mu = 1.000) allocation failure;
[380:0x7f3bd3df6000] 127 ms: Mark-Compact 41.7 (64.9) -> 38.2 (73.8) MB, pooled: 0 MB, 2.82 / 0.00 ms (+ 0.4 ms in 0 steps since start of marking, biggest step 0.0 ms, walltime since start of marking 12 ms) (average mu = 0.977, current mu = 0.977) finalize incremental marking via stack guard; GC in old space requested
[380:0x7f3bd3df6000] 148 ms: Scavenge 57.7 (77.6) -> 54.6 (77.6) MB, pooled: 0 MB, 4.69 / 0.00 ms (average mu = 0.977, current mu = 0.977) allocation failure;
[380:0x7f3bd3df6000] 159 ms: Scavenge 57.7 (77.6) -> 57.1 (89.6) MB, pooled: 0 MB, 7.86 / 0.00 ms (average mu = 0.977, current mu = 0.977) allocation failure;
[380:0x7f3bd3df6000] 172 ms: Scavenge 70.3 (89.6) -> 67.6 (91.6) MB, pooled: 0 MB, 3.94 / 0.00 ms (average mu = 0.977, current mu = 0.977) allocation failure;
[380:0x7f3bd3df6000] 182 ms: Scavenge 72.7 (91.6) -> 71.6 (102.1) MB, pooled: 0 MB, 5.92 / 0.00 ms (average mu = 0.977, current mu = 0.977) allocation failure;
[380:0x7f3bd3df6000] 194 ms: Scavenge 83.2 (102.1) -> 80.9 (106.3) MB, pooled: 0 MB, 4.03 / 0.00 ms (average mu = 0.977, current mu = 0.977) allocation failure;
[380:0x7f3bd3df6000] 204 ms: Scavenge 87.3 (106.3) -> 86.0 (115.6) MB, pooled: 0 MB, 4.54 / 0.00 ms (average mu = 0.977, current mu = 0.977) allocation failure;
[380:0x7f3bd3df6000] 216 ms: Scavenge 96.5 (115.6) -> 94.4 (120.8) MB, pooled: 0 MB, 4.04 / 0.00 ms (average mu = 0.977, current mu = 0.977) allocation failure;
[380:0x7f3bd3df6000] 235 ms: Scavenge 109.2 (128.3) -> 107.7 (137.1) MB, pooled: 0 MB, 5.21 / 0.00 ms (average mu = 0.977, current mu = 0.977) allocation failure;
[380:0x7f3bd3df6000] 255 ms: Scavenge (interleaved) 117.6 (137.1) -> 115.6 (143.3) MB, pooled: 0 MB, 6.53 / 0.00 ms (average mu = 0.977, current mu = 0.977) allocation failure;
[380:0x7f3bd3df6000] 273 ms: Scavenge (interleaved) 123.4 (143.3) -> 121.8 (151.6) MB, pooled: 0 MB, 7.72 / 0.00 ms (average mu = 0.977, current mu = 0.977) allocation failure;
[380:0x7f3bd3df6000] 279 ms: Mark-Compact 123.1 (151.6) -> 117.2 (152.7) MB, pooled: 0 MB, 3.91 / 0.00 ms (+ 0.7 ms in 0 steps since start of marking, biggest step 0.0 ms, walltime since start of marking 44 ms) (average mu = 0.975, current mu = 0.973) finalize incremental marking via stack guard; GC in old space requested
[380:0x7f3bd3df6000] 296 ms: Scavenge 132.9 (152.7) -> 129.8 (152.7) MB, pooled: 0 MB, 4.28 / 0.00 ms (average mu = 0.975, current mu = 0.973) allocation failure;
[380:0x7f3bd3df6000] 308 ms: Scavenge 132.9 (152.7) -> 132.2 (165.4) MB, pooled: 0 MB, 7.98 / 0.00 ms (average mu = 0.975, current mu = 0.973) allocation failure;
[380:0x7f3bd3df6000] 322 ms: Scavenge 145.4 (165.4) -> 142.8 (167.9) MB, pooled: 0 MB, 4.07 / 0.00 ms (average mu = 0.975, current mu = 0.973) allocation failure;
[380:0x7f3bd3df6000] 333 ms: Scavenge 147.9 (167.9) -> 146.9 (178.7) MB, pooled: 0 MB, 6.16 / 0.00 ms (average mu = 0.975, current mu = 0.973) allocation failure;
[380:0x7f3bd3df6000] 1047 ms: Scavenge 149.1 (178.7) -> 148.6 (182.7) MB, pooled: 0 MB, 6.11 / 0.00 ms (average mu = 0.975, current mu = 0.973) testing;
Mem after 100K iteration 232.23 MB
[380:0x7f3bd3df6000] 2046 ms: Scavenge 148.7 (182.7) -> 148.6 (184.7) MB, pooled: 0 MB, 3.56 / 0.00 ms (average mu = 0.975, current mu = 0.973) testing;
Mem after 100K iteration 234.25 MB
[380:0x7f3bd3df6000] 3043 ms: Scavenge 148.6 (184.7) -> 148.6 (184.7) MB, pooled: 0 MB, 0.95 / 0.00 ms (average mu = 0.975, current mu = 0.973) testing;
Mem after 100K iteration 234.24 MB
[380:0x7f3bd3df6000] 4045 ms: Scavenge 148.6 (184.7) -> 148.6 (184.7) MB, pooled: 0 MB, 0.76 / 0.00 ms (average mu = 0.975, current mu = 0.973) testing;
Mem after 100K iteration 234.25 MB
[380:0x7f3bd3df6000] 5047 ms: Scavenge 148.6 (184.7) -> 148.6 (184.7) MB, pooled: 0 MB, 1.83 / 0.00 ms (average mu = 0.975, current mu = 0.973) testing;
Mem after 100K iteration 234.31 MB
[380:0x7f3bd3df6000] 6047 ms: Scavenge 148.6 (184.7) -> 148.7 (184.7) MB, pooled: 0 MB, 1.13 / 0.00 ms (average mu = 0.975, current mu = 0.973) testing;
Mem after 100K iteration 234.3 MB
[380:0x7f3bd3df6000] 7047 ms: Scavenge 148.7 (184.7) -> 148.6 (184.7) MB, pooled: 0 MB, 0.78 / 0.00 ms (average mu = 0.975, current mu = 0.973) testing;
Mem after 100K iteration 234.29 MB
[380:0x7f3bd3df6000] 8050 ms: Scavenge 148.7 (184.7) -> 148.7 (153.7) MB, pooled: 31 MB, 1.55 / 0.00 ms (average mu = 0.975, current mu = 0.973) testing;
Mem after 100K iteration 234.29 MB
[380:0x7f3bd3df6000] 8463 ms: Mark-Compact (reduce) 148.9 (153.7) -> 82.7 (143.4) MB, pooled: 0 MB, 10.03 / 0.00 ms (+ 14.2 ms in 0 steps since start of marking, biggest step 0.0 ms, walltime since start of marking 27 ms) (average mu = 0.997, current mu = 0.997) finalize incremental marking via task; GC in old space requested
[380:0x7f3bd3df6000] 9051 ms: Scavenge 82.7 (143.4) -> 82.7 (144.4) MB, pooled: 0 MB, 1.57 / 0.00 ms (average mu = 0.997, current mu = 0.997) testing;
Mem after 100K iteration 148.13 MB
[380:0x7f3bd3df6000] 9094 ms: Mark-Compact (reduce) 82.7 (144.4) -> 82.7 (101.4) MB, pooled: 0 MB, 9.94 / 0.00 ms (+ 16.9 ms in 0 steps since start of marking, biggest step 0.0 ms, walltime since start of marking 30 ms) (average mu = 0.992, current mu = 0.958) finalize incremental marking via task; GC in old space requested
[380:0x7f3bd3df6000] 10051 ms: Scavenge 82.7 (101.4) -> 82.7 (101.4) MB, pooled: 1 MB, 1.02 / 0.00 ms (average mu = 0.992, current mu = 0.958) testing;
Mem after 100K iteration 141.08 MB
[380:0x7f3bd3df6000] 11051 ms: Scavenge 82.7 (101.4) -> 82.7 (101.4) MB, pooled: 1 MB, 0.37 / 0.00 ms (average mu = 0.992, current mu = 0.958) testing;
Mem after 100K iteration 141.08 MB
[380:0x7f3bd3df6000] 12052 ms: Scavenge 82.7 (101.4) -> 82.7 (101.4) MB, pooled: 1 MB, 0.42 / 0.00 ms (average mu = 0.992, current mu = 0.958) testing;
Mem after 100K iteration 141.09 MB
[380:0x7f3bd3df6000] 13053 ms: Scavenge 82.7 (101.4) -> 82.7 (101.4) MB, pooled: 1 MB, 0.84 / 0.00 ms (average mu = 0.992, current mu = 0.958) testing;
Mem after 100K iteration 141.09 MB
[380:0x7f3bd3df6000] 14054 ms: Scavenge 82.7 (101.4) -> 82.7 (101.4) MB, pooled: 1 MB, 0.42 / 0.00 ms (average mu = 0.992, current mu = 0.958) testing;
Mem after 100K iteration 141.09 MB
[380:0x7f3bd3df6000] 15056 ms: Scavenge 82.7 (101.4) -> 82.7 (101.4) MB, pooled: 1 MB, 0.74 / 0.00 ms (average mu = 0.992, current mu = 0.958) testing;
Mem after 100K iteration 141.09 MB
[380:0x7f3bd3df6000] 16057 ms: Scavenge 82.7 (101.4) -> 82.7 (101.4) MB, pooled: 1 MB, 0.76 / 0.00 ms (average mu = 0.992, current mu = 0.958) testing;
Mem after 100K iteration 141.09 MB
[380:0x7f3bd3df6000] 17059 ms: Scavenge 82.7 (101.4) -> 82.7 (101.4) MB, pooled: 1 MB, 0.63 / 0.00 ms (average mu = 0.992, current mu = 0.958) testing;
Mem after 100K iteration 141.09 MB
[380:0x7f3bd3df6000] 18060 ms: Scavenge 82.7 (101.4) -> 82.7 (101.4) MB, pooled: 1 MB, 0.59 / 0.00 ms (average mu = 0.992, current mu = 0.958) testing;
Mem after 100K iteration 141.09 MB
[380:0x7f3bd3df6000] 19061 ms: Scavenge 82.7 (101.4) -> 82.7 (101.4) MB, pooled: 1 MB, 0.41 / 0.00 ms (average mu = 0.992, current mu = 0.958) testing;
Mem after 100K iteration 141.09 MB
[380:0x7f3bd3df6000] 20062 ms: Scavenge 82.7 (101.4) -> 82.7 (101.4) MB, pooled: 1 MB, 0.64 / 0.00 ms (average mu = 0.992, current mu = 0.958) testing;
Mem after 100K iteration 141.09 MB
[380:0x7f3bd3df6000] 21063 ms: Scavenge 82.7 (101.4) -> 82.7 (101.4) MB, pooled: 1 MB, 0.81 / 0.00 ms (average mu = 0.992, current mu = 0.958) testing;
Mem after 100K iteration 141.09 MB
[380:0x7f3bd3df6000] 22063 ms: Scavenge 82.7 (101.4) -> 82.7 (101.4) MB, pooled: 1 MB, 0.43 / 0.00 ms (average mu = 0.992, current mu = 0.958) testing;
Mem after 100K iteration 141.09 MB
/app #Expected Behavior
All AbortController/AbortSignal should be destroyed.
Logs & Screenshots
Environment
Node: v22.3.0
Docker Image: node:apline
Additional context
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
confirmed-bugIssues and PRs for confirmed bugs.Issues and PRs for confirmed bugs.


Activity
ronag commentedon Sep 26, 2024
Can you try with node 22.9? I believe this was fixed quite recently.
mcollina commentedon Sep 26, 2024
Can you please include a reproduction?
System233 commentedon Oct 8, 2024
@ronag @mcollina I have edited the issue, please see the minimal reproducible code.
metcoder95 commentedon Oct 8, 2024
Shall we move this to Node?
mcollina commentedon Oct 9, 2024
Transfered.
geeksilva97 commentedon Oct 10, 2024
Interesting comment on this denoland/deno#24842 (comment)
geeksilva97 commentedon Oct 10, 2024
Part of the issue with this reproduction code may be that event listeners should be removed; otherwise, gcPersistentSignals will retain those.
If that's the case I think the issue becomes the same as reported for
AbortSignal.any.System233 commentedon Oct 10, 2024
If you add an event listener directly to
AbortSignal, no memory leak occurs, but only onAbortSignal.any.I did fix the problem temporarily by removing the event listener on
AbortSignal.any.mika-fischer commentedon Oct 10, 2024
I can confirm that leaving a listener attached to the
AbortSignalof anAbortControllerdoes not cause a leak, but leaving a handler attached to theAbortSignalreturned byAbortSignal.anydoes cause a leak.Better repro:
System233 commentedon Oct 10, 2024
GC takes a while to triggerSorry, I ran the test and the GC triggers faster than I expected to keep the memory at a certain level
mika-fischer commentedon Oct 10, 2024
Sure, but this seems to be a real leak. Run this with
--expose-gc --trace-gc:I pretty quickly come to:
System233 commentedon Oct 10, 2024
Yes, I can get the same test results here, I didn't make it clear just now that I was testing the GC speed after uncommenting the
//composedSignal.removeEventListener('abort', handler);line, sorry.geeksilva97 commentedon Oct 14, 2024
I was looking into this. This is done by definition: https://github.com/nodejs/node/blob/main/lib/internal/abort_controller.js#L278-L281
I wonder if changing this would violate the spec
System233 commentedon Oct 14, 2024
It seems that
gcPersistentSignalsexists solely to address the issue whereAbortSignal.timeout()would block the process from exiting?https://dom.spec.whatwg.org/#abortsignal-abort-algorithms
I don't understand why this needs to be made so complicated. My intuition tells me that Node.js does not need to implement this functionality deliberately, as setTimeout itself can directly hold a strong reference to AbortSignal.timeout, thus preventing AbortSignal from being garbage collected due to the lack of a strong reference. Additionally, AbortSignal.any should also hold a strong reference to the signals.
Furthermore, I have another question: When AbortSignal.timeout is active and there are abort event listeners present, the DOM standard states that AbortSignal must not be garbage collected at this time. Since AbortSignal cannot be garbage collected during this period, shouldn't AbortSignal.timeout be able to block the process exit? Currently, AbortSignal.timeout does not block the process exit.
mika-fischer commentedon Oct 14, 2024
IMO the issue here and the issue with an
AbortSignal.timeoutthat has not timed out yet are completely different. It helps to consider the "settled" concept for signals as defined here: chromium/chromium@d5b7539AbortControlleris GCed.aborton theAbortController, the signal cannot possibly trigger in the future.mika-fischer commentedon Oct 14, 2024
See especially here for more details: https://docs.google.com/document/d/1LvmsBLV85p-PhSGvTH-YwgD6onuhh1VXLg8jPlH32H4/edit?pli=1#heading=h.7ut6obnf9fz0
geeksilva97 commentedon Oct 28, 2024
Thanks for the reference, @mika-fischer . This week I'll take a look into this. I've got some ideas.
geeksilva97 commentedon Nov 28, 2024
FYI, PR is done
lib: clean up persisted signals when they are settled
lib: clean up persisted signals when they are settled
lib: clean up persisted signals when they are settled
lib: clean up persisted signals when they are settled
fix(langgraph): memory leak when using AbortSignal.any(...)