Module A-2·24 min read

The four patterns behind 95% of Node.js memory leaks: event listener accumulation (on() without off() per request), closure scope retaining large objects, unbounded in-memory caches without TTL/LRU, and circular reference traps — with WeakRef/WeakMap solutions and a Jest-compatible memory leak test pattern.

Module P-15 — Memory Leak Prevention Patterns

What this module covers: The most expensive Node.js memory leak is the one you ship in your first version and discover 6 weeks later when your pods start OOMing at 3am. clinic.js and flame graphs help you find a leak after it exists. This module teaches you not to write the leak in the first place.


Why Node.js Memory Leaks Are Different

V8's garbage collector handles object lifecycle automatically. Memory leaks in Node.js are not C-style use-after-free bugs. They're retention bugs: objects that are still referenced somewhere in the heap even though the application is done with them. The GC cannot collect what is still reachable.

The four patterns behind 95% of Node.js memory leaks:

  1. Event listener accumulation
  2. Closure scope retaining large objects
  3. Unbounded in-memory caches
  4. Circular references preventing GC

Pattern 1: Event Listener Accumulation

Every emitter.on('event', handler) creates a reference from the emitter to the handler function. If the emitter outlives the handler's intended scope (which it usually does), the handler is never GC'd. Multiply this by every request that creates a new handler.

The bug:

javascript

After 1000 requests, someGlobalEmitter has 1000 accumulated listeners. process.getMaxListeners() will warn at 11, but warnings don't stop the leak.

The fix:

javascript

Detection:

javascript

The once() pattern for event listeners that should fire exactly once:

javascript

AbortSignal-aware listeners:

javascript

Pattern 2: Closure Scope Retaining Large Objects

A closure captures its surrounding scope, not individual variables. If the scope contains a large object and the closure outlives its usefulness, the large object cannot be GC'd.

The bug:

javascript

The fix: capture only what you need

javascript

The rule: in a function that handles large objects (request bodies, file buffers, database result sets), extract only the primitives you need before passing callbacks to longer-lived systems.


Pattern 3: Unbounded In-Memory Caches

An in-memory Map that grows forever is a memory leak with a polite name. It's a cache until it's a problem, then it's an OOM.

The bug:

javascript

With 100,000 products, this cache holds 100,000 entries with no eviction policy.

Fix: LRU cache with bounded size

javascript

Session caches and connection caches: Apply the same pattern. Redis clients cached by connection string: cap at 50. Database connection pools: always use pool.end() on shutdown.

The in-process Map as a shared singleton: If your module exports a Map, it lives for the entire process lifetime. Every test that doesn't clear it accumulates state. Add a clear() function and call it in afterEach in tests.


Pattern 4: Circular References

Modern V8 handles circular references in pure JavaScript objects (two objects referencing each other). GC collects them when the entire cycle becomes unreachable. The problem arises when circular references involve non-GC'd native resources (streams, buffers, timers).

The subtle bug:

javascript

Fix: WeakRef for non-owning references

javascript

WeakMap for metadata attached to objects you don't own:

javascript

The Memory Leak Test Pattern

Verify a suspected memory leak with a repetition test:

javascript

Run with --expose-gc: node --expose-gc node_modules/.bin/jest memory-leak.test.ts


Monitoring in Production

javascript

Set pod memory limits at 2x your typical heap usage. Alert at 70% of limit. Kill and restart at 90%.


Knowledge Check

Why does failing to call removeListener (or off) on a global EventEmitter within an HTTP request handler lead to a memory leak?


Which of the following is the safest way to store request-specific metadata on an Express request object without risking a memory leak if the request is unexpectedly aborted?


What is the purpose of measuring memory leak potential across multiple requests in a Jest test using --expose-gc and global.gc()?

Test your knowledge with more question sets

Sign in to access a wider variety of questions and get notified when new practice sets are added to this module.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.