Question Details

No question body available.

Tags

javascript node.js async-await memory-leaks async-hooks

Answers (1)

Accepted Answer Available
Accepted Answer
April 18, 2025 Score: 1 Rep: 464 Quality: High Completeness: 80%

EDIT 2:

I finally have the full picture thanks to a colleague's hawk eyes. In the codebase, AsyncLocalStorage is really from the npm package async-local-storage here. This package uses node's async hooks to create a new context (a vanilla js object) on every async operation start and keeps a reference to the parent's storage via the parent async resource's execution context id. You can see this here. Because we do this on a loop infinitely, this keeps piling up, impossible for the GC to clean up.

One way to get rid of the chaining would be to call scope() on every iteration.

EDIT 1:

I finally have half an answer why this was happening. In the codebase, AsyncLocalStorage is enabled. This creates a context for each async call and chains past contexts for consecutive calls. I can reproduce this behavior with this isolated snippet:

import { enable } from 'async-local-storage';

enable();

async function main() { while (true) { await new Promise((resolve) => setTimeout(resolve, 100 + 100 Math.random()), ); } }

main()

What I want to understand is why the contexts are being chained since this promises here get resolved before creating the next one.

Previous answer:

I have settled for a setInterval based approach that waits for the processing code to finish before running it again.

const driver = (() => {
    if (!this.polling) this.pollQ();
  }).bind(this);

setInterval(driver, 100 + 100 Math.random());

and

async pollQ() {
    this.polling = true;
    try {
      // awaits
    } catch (e) {
      // awaits
    } finally {
      this.polling = false;
    }
  }

This heap is stable with this. But I'd still like to know why the original code grows the heap non stop.