DEV Community

Cover image for Node.js Timeouts and Memory Leaks
Srijan Karki
Srijan Karki

Posted on

Node.js Timeouts and Memory Leaks

Introduction

  • Issue Overview: The way Node.js handles timeouts can lead to significant memory leaks.
  • Background: The setTimeout API is commonly used in both browsers and Node.js. While it works similarly, Node.js returns a more complex object, which can cause problems.

Basic Timeout API

  • In Browsers:
    • Token: A simple number representing the timeout ID.
  const token = setTimeout(() => {}, 100);
  clearTimeout(token);
Enter fullscreen mode Exit fullscreen mode
  • In Node.js:
    • Token: An object with multiple properties and references.
  const token = setTimeout(() => {});
  console.log(token);
Enter fullscreen mode Exit fullscreen mode

Example of Timeout Object in Node.js

Timeout {
  _idleTimeout: 1,
  _idlePrev: [TimersList],
  _idleNext: [TimersList],
  _idleStart: 4312,
  _onTimeout: [Function (anonymous)],
  _timerArgs: undefined,
  _repeat: null,
  _destroyed: false,
  [Symbol(refed)]: true,
  [Symbol(kHasPrimitive)]: false,
  [Symbol(asyncId)]: 78,
  [Symbol(triggerId)]: 6
}
Enter fullscreen mode Exit fullscreen mode
  • Properties: Includes metadata about the timeout, references to other objects, and functions.
  • Issue: These references prevent the timeout object from being garbage collected even after it’s cleared or completed.

Class Example Leading to Memory Leak

class MyThing {
  constructor() {
    this.timeout = setTimeout(() => { /*...*/ }, INTERVAL);
  }

  clearTimeout() {
    clearTimeout(this.timeout);
  }
}
Enter fullscreen mode Exit fullscreen mode
  • Persistent Reference: The Timeout object persists in memory because it is an object with references, not a simple number.

Impact of AsyncLocalStorage

  • AsyncLocalStorage: A new API that attaches additional state to timeouts, promises, and other asynchronous operations.
  • Example:
  const { AsyncLocalStorage } = require('node:async_hooks');
  const als = new AsyncLocalStorage();

  let t;
  als.run([...Array(10000)], () => {
    t = setTimeout(() => {
      const theArray = als.getStore();
    }, 100);
  });
Enter fullscreen mode Exit fullscreen mode
  • Result: The timeout object now holds a reference to a large array via a custom Symbol, which persists even after the timeout is cleared or completes.
  Timeout {
    [Symbol(kResourceStore)]: [Array] // reference to that large array is held here
  }
Enter fullscreen mode Exit fullscreen mode

Suggested Fix: Using Primitive IDs

  • Approach: Convert the Timeout object to a number to avoid holding references.
  class MyThing {
    constructor() {
      this.timeout = +setTimeout(() => { /*...*/ }, INTERVAL);
    }

    clearTimeout() {
      clearTimeout(this.timeout);
    }
  }
Enter fullscreen mode Exit fullscreen mode
  • Current Problem: Due to a bug in Node.js, this approach currently causes an unrecoverable memory leak.

Workaround: Aggressive Nullification

  • Strategy: Manually clear the timeout reference to help garbage collection.
  class MyThing {
    constructor() {
      this.timeout = setTimeout(() => {
        this.timeout = null;
        // Additional logic
      }, INTERVAL);
    }

    clearTimeout() {
      if (this.timeout) {
        clearTimeout(this.timeout);
        this.timeout = null;
      }
    }
  }
Enter fullscreen mode Exit fullscreen mode

Broader Implications

  • Widespread Issue: Many Node.js applications use timeouts and intervals, increasing the risk of memory leaks.
  • Hot Code Reloading: Long-lasting or recurring timeouts can exacerbate the problem.
  • Next.js Workaround: Patches setTimeout and setInterval to clear intervals periodically, but can still encounter the Node.js bug.

Long-Term Considerations

  • API Improvements: Node.js could return a lightweight proxy object instead of the full Timeout object, which would be easier to manage and less prone to leaks.
  • AsyncLocalStorage Management: Providing APIs to prevent unnecessary state propagation can help reduce memory leaks.

Conclusion

  • Memory Management: Developers need to carefully manage timeouts and their references to avoid memory leaks.
  • Awaiting Node.js Fix: A permanent fix for the underlying Node.js bug is crucial for effective memory management.

Understanding these nuances and adopting best practices can help mitigate memory leaks in Node.js applications, ensuring better performance and stability.

Top comments (0)