---
# === IDENTITY ===
id: software/debugging/nodejs-memory-leaks/2026
canonical_question: "How do I find and fix memory leaks in Node.js?"
aliases:
  - "Node.js memory leak"
  - "Node.js heap out of memory"
  - "Node.js process memory growing"
  - "Node.js RSS keeps increasing"
  - "Node.js garbage collection not working"
  - "Node.js FATAL ERROR Reached heap limit"
  - "Node.js EventEmitter memory leak warning"
  - "Node.js OOM kill in Docker container"
entity_type: software_reference
domain: software > debugging > nodejs_memory_leaks
region: global
jurisdiction: global
temporal_scope: 2020-2026

# === VERIFICATION ===
last_verified: 2026-05-17
confidence: 0.92
version: 2.1
first_published: 2026-02-17

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: stable
  last_breaking_change: "Node.js 24 LTS (April 2026) — V8 13.x rolls in CppHeap GC improvements; some native-addon workloads see different allocation timing vs Node 22"
  next_review: 2026-11-13
  change_sensitivity: medium

# === CONSTRAINTS ===
constraints:
  - "Never increase maxListeners to suppress MaxListenersExceededWarning — it masks a real leak; find and remove the accumulating listeners instead"
  - "Never take heap snapshots under production load — v8.writeHeapSnapshot() pauses the event loop for seconds to minutes proportional to heap size"
  - "In containers set --max-old-space-size to 75% of cgroup memory limit — Node.js 20+ is cgroup-aware but V8 may still OOM-kill before GC runs"
  - "WeakRef and FinalizationRegistry are not substitutes for explicit cleanup — weak reference finalization timing is non-deterministic"
  - "Node.js 22 RSS growth under sustained CPU load is V8's page-caching optimization, not a memory leak — verify with heapUsed, not RSS"
  - "Do not use --expose-gc in production — manual GC calls block the event loop and mask real allocation patterns"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "High CPU but stable memory usage — not a memory leak"
    use_instead: "Use CPU profiling: clinic flame or 0x"
  - condition: "OOM during expected large data processing — not a leak, just insufficient memory"
    use_instead: "Use streaming/pagination to reduce peak memory"
  - condition: "Python process with memory growth — different runtime"
    use_instead: "software/debugging/python-memory-leaks/2026"
  - condition: "Container OOM-killed but Node.js heapUsed is stable — cgroup limit issue"
    use_instead: "software/debugging/docker-oomkilled/2026"

# === AGENT HINTS ===
inputs_needed:
  - key: node_version
    question: "Which Node.js version are you running?"
    type: choice
    options: ["Node.js 18 LTS", "Node.js 20 LTS", "Node.js 22 Current", "Node.js 23+"]
  - key: environment
    question: "Where is the application running?"
    type: choice
    options: ["Local development", "Docker/Kubernetes container", "Cloud VM (EC2, GCE)", "Serverless (Lambda, Cloud Functions)"]
  - key: symptom
    question: "What symptom are you observing?"
    type: choice
    options: ["heapUsed growing monotonically", "MaxListenersExceededWarning", "FATAL ERROR heap limit", "OOM kill in container", "RSS growing but heapUsed stable"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/debugging/nodejs-memory-leaks/2026"
suggested_citation: "Source: knowledgelib.io — AI Knowledge Library (verified 2026-05-17)"

# === RELATED UNITS ===
related_kos:
  related_to:
    - id: "software/debugging/nodejs-econnrefused/2026"
      label: "Node.js ECONNREFUSED"
    - id: "software/debugging/nodejs-unhandled-promises/2026"
      label: "Node.js Unhandled Promise Rejections"
    - id: "software/debugging/redis-memory-issues/2026"
      label: "Redis Memory Issues"
  solves:
    - id: "software/debugging/docker-oomkilled/2026"
      label: "Docker OOMKilled"
  alternative_to:
    - id: "software/debugging/python-memory-leaks/2026"
      label: "Python Memory Leaks (different runtime)"
    - id: "software/debugging/go-goroutine-leak/2026"
      label: "Go Goroutine Leak (different runtime)"
  often_confused_with:
    - id: "software/debugging/java-outofmemoryerror/2026"
      label: "Java OutOfMemoryError (JVM, not V8)"
    - id: "software/debugging/nextjs-build-failures/2026"
      label: "Next.js Build Failures (build-time, not runtime)"

# === SOURCES (5-8 authoritative sources) ===
sources:
  - id: src1
    title: "Node.js — Diagnostics: Memory"
    author: Node.js Foundation
    url: https://nodejs.org/en/learn/diagnostics/memory
    type: official_docs
    published: 2025-06-01
    reliability: authoritative
  - id: src2
    title: "Debugging Memory Leaks in Node.js — A Practical Guide"
    author: Better Stack
    url: https://betterstack.com/community/guides/scaling-nodejs/high-performance-nodejs/nodejs-memory-leaks/
    type: technical_blog
    published: 2025-03-01
    reliability: high
  - id: src3
    title: "Finding And Fixing Node.js Memory Leaks: A Practical Guide"
    author: AppSignal
    url: https://blog.appsignal.com/2024/08/14/finding-and-fixing-nodejs-memory-leaks.html
    type: technical_blog
    published: 2024-08-14
    reliability: high
  - id: src4
    title: "Sematext — Node.js Memory Leaks: Identifying and Fixing"
    author: Sematext
    url: https://sematext.com/blog/nodejs-memory-leaks/
    type: technical_blog
    published: 2025-01-01
    reliability: high
  - id: src5
    title: "Node.js — Understanding process.memoryUsage()"
    author: Node.js Foundation
    url: https://nodejs.org/api/process.html#processmemoryusage
    type: official_docs
    published: 2025-06-01
    reliability: authoritative
  - id: src6
    title: "Clinic.js — Node.js Performance Profiling"
    author: NearForm
    url: https://clinicjs.org/
    type: community_resource
    published: 2025-01-01
    reliability: high
  - id: src7
    title: "Chrome DevTools — Memory Panel Documentation"
    author: Google
    url: https://developer.chrome.com/docs/devtools/memory
    type: official_docs
    published: 2025-06-01
    reliability: authoritative
  - id: src8
    title: "Understanding Node.js 22 Memory Behavior and Upstream Contribution"
    author: Cribl
    url: https://cribl.io/blog/understanding-node-js-22-memory-behavior-and-our-upstream-contribution/
    type: technical_blog
    published: 2025-09-01
    reliability: high
---

# How to Find and Fix Memory Leaks in Node.js

## TL;DR

- **Bottom line**: Node.js memory leaks occur when objects remain referenced after they're no longer needed, preventing V8's garbage collector from reclaiming memory. The 5 most common causes: (1) forgotten event listeners, (2) growing global caches without eviction, (3) closures capturing large scopes, (4) uncleared timers (`setInterval`/`setTimeout`), (5) unhandled streams and unclosed connections.
- **Key tool/command**: `node --inspect app.js` then open Chrome DevTools Memory tab, take two heap snapshots 5 minutes apart, compare by "# Delta" to find growing objects.
- **Watch out for**: `process.memoryUsage().heapUsed` growing monotonically over hours -- this indicates a leak even if GC is running. RSS growth alone can be misleading: Node.js 22 caches free V8 pages under sustained load (not a leak). [src8]
- **Works with**: Node.js 18 LTS, Node.js 20 LTS, Node.js 22 Current. Tools: Chrome DevTools, Clinic.js, `heapdump`, `v8-profiler-next`.

## Constraints
<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

- Never increase `maxListeners` to suppress `MaxListenersExceededWarning` -- it masks a real leak; find and remove the accumulating listeners instead [src2, src3]
- Never take heap snapshots under production load -- `v8.writeHeapSnapshot()` pauses the event loop for seconds to minutes proportional to heap size [src7]
- In containers set `--max-old-space-size` to 75% of cgroup memory limit -- Node.js 20+ is cgroup-aware but V8 may still OOM-kill before GC runs [src1]
- `WeakRef` and `FinalizationRegistry` are not substitutes for explicit cleanup -- weak reference finalization timing is non-deterministic [src1]
- Node.js 22 RSS growth under sustained CPU load is V8's page-caching optimization, not a memory leak -- verify with `heapUsed`, not `RSS` [src8]
- Do not use `--expose-gc` in production -- manual GC calls block the event loop and mask real allocation patterns [src1]

## Quick Reference

| # | Cause | Likelihood | Signature | Fix |
|---|---|---|---|---|
| 1 | Forgotten event listeners | ~25% of cases | `MaxListenersExceededWarning` or growing listener count | Call `removeListener()`/`off()` in cleanup; use `once()` for one-shot events [src2, src3] |
| 2 | Unbounded cache / Map / object | ~20% of cases | Growing object count in heap snapshot | Use LRU cache (`lru-cache` package) or `WeakMap` for object keys [src3, src4] |
| 3 | Closures capturing large scope | ~15% of cases | Retained size of closure objects growing in heap | Narrow closure scope; nullify large variables after use [src2, src4] |
| 4 | Uncleared `setInterval`/`setTimeout` | ~12% of cases | Timer count grows; callbacks reference stale objects | Call `clearInterval()`/`clearTimeout()` on cleanup; use `AbortSignal` in Node 20+ [src2, src3] |
| 5 | Unhandled/unconsumed streams | ~8% of cases | Internal buffer growing; `pause()` never triggered | Implement backpressure; call `stream.destroy()` on error [src4] |
| 6 | Global variable accumulation | ~7% of cases | Objects in global scope never freed | Use `let`/`const` in functions; avoid `global.xxx` patterns [src2, src3] |
| 7 | Unclosed DB/network connections | ~5% of cases | Connection pool exhaustion; socket handles grow | Use connection pooling with limits; close on shutdown [src3] |
| 8 | Circular references (pre-WeakRef) | ~3% of cases | Object pairs mutually referencing | Use `WeakRef`/`WeakMap`; break cycles explicitly [src4] |
| 9 | Large Buffer allocation | ~3% of cases | `external` memory growing in `process.memoryUsage()` | Use streaming I/O; avoid loading full files into memory [src4] |
| 10 | Promise chains never resolved | ~2% of cases | Pending promise count growing | Always resolve/reject; add `.catch()`; use `AbortController` for timeouts [src2] |

## Decision Tree

```
START
├── Is heapUsed growing steadily over time (monotonically)?
│   ├── YES → Likely a memory leak — continue diagnosis ↓
│   └── NO → Might be normal GC behavior; check if RSS is growing without heapUsed growth
│       └── RSS growing, heapUsed stable → V8 page caching (Node.js 22+), not a leak [src8]
│           └── Confirm: introduce brief idle periods → if RSS drops, it's V8 optimization
├── Do you see "MaxListenersExceededWarning" in logs?
│   ├── YES → Event listener leak. Find where listeners are added without removal [src2, src3]
│   └── NO ↓
├── Running in Docker/K8s and getting OOM-killed?
│   ├── YES → Check --max-old-space-size (set to 75% of container limit) [src1]
│   │   └── Already set? → True leak — continue diagnosis ↓
│   └── NO ↓
├── Does memory growth correlate with specific requests/events?
│   ├── YES → Trace the request handler
│   │   ├── Using in-memory cache? → Add eviction policy (LRU, TTL) [src3, src4]
│   │   ├── Creating timers per request? → Clear them in response/error handler [src2]
│   │   └── Opening connections per request? → Use connection pool [src3]
│   └── NO → Global or startup-time leak ↓
├── Take two heap snapshots 5 minutes apart → compare
│   ├── Growing object type identified → Trace by retainer path in DevTools [src7]
│   └── No clear growing type → Use allocation timeline profiling [src7]
└── Still unclear → Use Clinic.js HeapProfiler for automated analysis [src6]
```

## Step-by-Step Guide

### 1. Confirm there is actually a memory leak

Not all memory growth is a leak. V8's garbage collector works in cycles, so memory usage fluctuates. A true leak shows monotonically growing `heapUsed` over extended periods. In Node.js 22+, RSS may grow under sustained CPU load due to V8's page-caching optimization -- this is not a leak. [src1, src5, src8]

```javascript
// Add this to your app for basic monitoring
setInterval(() => {
  const mem = process.memoryUsage();
  console.log(JSON.stringify({
    rss: Math.round(mem.rss / 1024 / 1024) + 'MB',
    heapTotal: Math.round(mem.heapTotal / 1024 / 1024) + 'MB',
    heapUsed: Math.round(mem.heapUsed / 1024 / 1024) + 'MB',
    external: Math.round(mem.external / 1024 / 1024) + 'MB',
  }));
}, 10000);
```

**Verify**: If `heapUsed` keeps growing over 30+ minutes under steady load → you have a leak.

### 2. Start with Chrome DevTools heap snapshots

Connect Chrome DevTools to your Node.js process for interactive heap analysis. [src7]

```bash
# Start your app with --inspect
node --inspect app.js

# Or attach to a running process
kill -USR1 <PID>  # Linux/macOS — enables inspector on default port 9229
```

Then open `chrome://inspect` in Chrome → click "inspect" on your target → go to the **Memory** tab.

**Verify**: DevTools connects and shows heap statistics.

### 3. Take comparison heap snapshots

The most effective technique: take two heap snapshots with time/load in between, then compare them. Force GC before each snapshot for accuracy. [src7]

```
1. Open Memory tab in Chrome DevTools
2. Force GC (click trash icon) before first snapshot
3. Select "Heap snapshot" → click "Take snapshot" → label it "Baseline"
4. Exercise your app (send requests, run operations for 5 minutes)
5. Force GC again → take another snapshot → label it "After load"
6. Select the second snapshot → change view to "Comparison"
7. Sort by "# Delta" (descending) → objects with positive delta are growing
8. Click on growing objects → inspect "Retainers" to find what holds references
```

**Verify**: You should see specific constructor names with positive deltas -- these are your leaking objects.

### 4. Check for common leak patterns

Search your codebase for the most common leak sources. [src2, src3]

```bash
# Event listeners added without removal
grep -rn "\.on(" --include="*.js" --include="*.ts" src/ | head -30
grep -rn "\.addEventListener" --include="*.js" --include="*.ts" src/

# Timers without cleanup
grep -rn "setInterval\|setTimeout" --include="*.js" --include="*.ts" src/
grep -rn "clearInterval\|clearTimeout" --include="*.js" --include="*.ts" src/

# Global variable assignments
grep -rn "global\.\|globalThis\." --include="*.js" --include="*.ts" src/

# In-memory caches
grep -rn "new Map()\|= {}" --include="*.js" --include="*.ts" src/ | \
  grep -iv "const.*=.*{}" | head -20
```

**Verify**: Each `setInterval`/`.on()` has a corresponding `clearInterval`/`.off()` in cleanup code.

### 5. Fix event listener leaks

The most common leak: adding listeners in request handlers without removing them. [src2, src3]

```javascript
// ❌ WRONG — listener added per request, never removed
app.get('/data', (req, res) => {
  database.on('update', (data) => {
    // This listener accumulates with every request!
    res.write(data);
  });
});

// ✅ CORRECT — remove listener when response ends
app.get('/data', (req, res) => {
  const onUpdate = (data) => res.write(data);
  database.on('update', onUpdate);

  res.on('close', () => {
    database.off('update', onUpdate); // Clean up!
  });
});
```

**Verify**: `emitter.listenerCount('eventName')` stays constant under load.

### 6. Fix unbounded cache leaks

In-memory caches that grow forever are a classic leak. [src3, src4]

```javascript
// ❌ WRONG — Map grows forever
const cache = new Map();
function getData(key) {
  if (cache.has(key)) return cache.get(key);
  const data = fetchFromDB(key);
  cache.set(key, data);  // Never evicted!
  return data;
}

// ✅ CORRECT — LRU cache with max size
import { LRUCache } from 'lru-cache';
const cache = new LRUCache({ max: 1000, ttl: 1000 * 60 * 5 }); // 1000 items, 5 min TTL

function getData(key) {
  if (cache.has(key)) return cache.get(key);
  const data = fetchFromDB(key);
  cache.set(key, data);
  return data;
}
```

**Verify**: `cache.size` stays bounded under sustained load.

### 7. Use Clinic.js for automated diagnosis

When manual analysis is difficult, Clinic.js can automatically diagnose memory issues. [src6]

```bash
# Install Clinic.js
npm install -g clinic

# Run heap profiler — generates interactive report
clinic heapprofile -- node app.js

# Run doctor for overall health check
clinic doctor -- node app.js

# After exercising the app, press Ctrl+C to generate the report
# It opens in your browser with visualizations of growing allocations
```

**Verify**: Clinic.js report highlights the source file and line where allocations are growing.

## Decision Logic

### If `heapUsed` is growing monotonically over 30+ minutes and `MaxListenersExceededWarning` appears in logs
--> Event listener leak. Use `emitter.listenerCount()` to confirm; refactor to use `once()` or explicit `off()` cleanup tied to request lifecycle. [src2, src3]

### If RSS grows but `heapUsed` is stable on Node.js 22+
--> Not a leak — V8's page-caching optimization. Verify by introducing brief idle periods; RSS should drop. Do not increase `--max-old-space-size`. [src8]

### If container is OOM-killed but `heapUsed` stays well under the heap limit
--> cgroup/container memory limit issue, not a JS leak. Set `--max-old-space-size` to 75% of container limit; check external memory (Buffers, native addons) growth. [src1]

### If `heapUsed` grows correlated with specific request paths
--> Per-request leak. Trace the handler for in-memory caches without eviction, timers without `clearInterval`, or DB connections without pooling. [src3, src4]

### If two heap snapshots show specific constructor names with positive `# Delta`
--> Inspect Retainers panel in Chrome DevTools to find what holds the references. Trace the retainer chain back to the root cause. [src7]

### If manual heap snapshot analysis is too noisy or production-only
--> Run `clinic heapprofile -- node app.js` for automated diagnosis with source-line attribution. Avoid taking heap snapshots under load — use `--heapsnapshot-near-heap-limit=N` to auto-capture before OOM. [src6, src7]

### If `external` memory in `process.memoryUsage()` is growing
--> Native memory leak in Buffers or C++ addons, not the JS heap. Audit `Buffer.alloc()` calls and native-module usage. Heap snapshots will not show this — use `--heap-prof` or a native profiler. [src5]

### If you're on Node.js 24 LTS and seeing GC behaviour different from Node 22
--> Node 24's V8 13.x changed CppHeap GC timing for native-addon workloads. Verify the leak with `heapUsed` (not RSS) and re-test under steady load before attributing to your code. [src8]

## Code Examples

### Express.js: Fixing middleware memory leak

> Full script: [express-js-fixing-middleware-memory-leak.js](scripts/express-js-fixing-middleware-memory-leak.js) (25 lines)

```javascript
// Input:  Express middleware leaking memory via event listeners
// Output: Properly cleaned up middleware
const express = require('express');
const EventEmitter = require('events');
const app = express();
# ... (see full script)
```

### Production monitoring: Memory leak detection script

> Full script: [production-monitoring-memory-leak-detection-script.js](scripts/production-monitoring-memory-leak-detection-script.js) (31 lines)

```javascript
// Input:  Need to detect memory leaks in production without DevTools
// Output: Automated monitoring with alerts
const LEAK_THRESHOLD_MB = 50;  // Alert if heap grows 50MB in window
const CHECK_INTERVAL_MS = 30000;  // Check every 30 seconds
const WINDOW_SIZE = 20;  // Track last 20 measurements
# ... (see full script)
```

### Graceful shutdown: Preventing connection leaks

> Full script: [graceful-shutdown-preventing-connection-leaks.js](scripts/graceful-shutdown-preventing-connection-leaks.js) (34 lines)

```javascript
// Input:  Server with DB and Redis connections that leak on restart
// Output: Graceful shutdown with comprehensive cleanup
const http = require('http');
const { Pool } = require('pg');
const Redis = require('ioredis');
# ... (see full script)
```

## Anti-Patterns

### Wrong: Global cache without eviction

```javascript
// ❌ BAD — grows forever, guaranteed memory leak [src3, src4]
const userCache = {};

app.get('/user/:id', async (req, res) => {
  if (!userCache[req.params.id]) {
    userCache[req.params.id] = await db.getUser(req.params.id);
  }
  res.json(userCache[req.params.id]);
});
// After 100,000 unique users → cache holds all 100,000 in memory
```

### Correct: Bounded cache with TTL

```javascript
// ✅ GOOD — bounded size + automatic expiration [src3, src4]
import { LRUCache } from 'lru-cache';

const userCache = new LRUCache({
  max: 500,                // At most 500 entries
  ttl: 1000 * 60 * 10,    // Expire after 10 minutes
  updateAgeOnGet: true,    // Reset TTL on access
});

app.get('/user/:id', async (req, res) => {
  let user = userCache.get(req.params.id);
  if (!user) {
    user = await db.getUser(req.params.id);
    userCache.set(req.params.id, user);
  }
  res.json(user);
});
```

### Wrong: Event listeners in request handlers

```javascript
// ❌ BAD — adds a new listener on every request, never removes [src2, src3]
const priceEmitter = new EventEmitter();

app.get('/price', (req, res) => {
  priceEmitter.on('update', (price) => {
    // After 10,000 requests → 10,000 listeners on 'update'
    console.log('New price:', price);
  });
  res.json({ price: currentPrice });
});
```

### Correct: Use once() or clean up listeners

```javascript
// ✅ GOOD — once() auto-removes after first call [src2, src3]
app.get('/price', (req, res) => {
  priceEmitter.once('update', (price) => {
    // Automatically removed after one invocation
    console.log('New price:', price);
  });
  res.json({ price: currentPrice });
});

// ✅ ALSO GOOD — explicit cleanup
app.get('/price-stream', (req, res) => {
  const handler = (price) => res.write(`data: ${price}\n\n`);
  priceEmitter.on('update', handler);
  req.on('close', () => priceEmitter.off('update', handler));
});
```

### Wrong: Timer per request without cleanup

```javascript
// ❌ BAD — timer keeps running after response is sent [src2]
app.get('/status', (req, res) => {
  setInterval(() => {
    // This interval runs FOREVER for each request
    checkStatus().then(s => console.log(s));
  }, 5000);
  res.json({ status: 'ok' });
});
```

### Correct: Clear timers on cleanup

```javascript
// ✅ GOOD — timer cleared when request ends [src2]
app.get('/status', (req, res) => {
  const timer = setInterval(() => {
    checkStatus().then(s => console.log(s));
  }, 5000);

  res.on('finish', () => clearInterval(timer));
  res.on('close', () => clearInterval(timer));
  res.json({ status: 'ok' });
});
```

## Common Pitfalls

- **Confusing RSS with heap**: `rss` (Resident Set Size) includes stack, code segments, and V8's reserved-but-unused heap. `heapUsed` is the actual live object memory. Track `heapUsed` for leak detection, not `rss`. In Node.js 22+, V8 caches free pages under sustained load, causing RSS to grow even without a leak. [src1, src5, src8]
- **Event listener limit warning ignored**: Node.js warns at 11+ listeners on a single event -- this is often a leak signal. Don't increase `maxListeners` to suppress it; find and fix the leak. [src2, src3]
- **Using `process.memoryUsage()` in production without alerting**: Logging memory stats is useless without automated threshold alerts. Set up monitoring that pages you when `heapUsed` exceeds a baseline. [src1]
- **Heap snapshots in production**: Taking a heap snapshot pauses the event loop for seconds to minutes (proportional to heap size). Use `v8.writeHeapSnapshot()` carefully -- never under high load. Use `--heapsnapshot-near-heap-limit=N` to auto-capture before OOM instead. [src7, src1]
- **`--max-old-space-size` as a "fix"**: Increasing heap size delays the crash but doesn't fix the leak. Use it only as a stopgap while you find the root cause. [src1]
- **Closures in hot paths**: A closure in a frequently-called function (like a request handler) that captures a large object from outer scope keeps that object alive as long as the closure exists. [src2, src4]
- **Not forcing GC before heap snapshots**: Without forcing GC (`--expose-gc` + `global.gc()`) before each snapshot, comparison data includes unreachable objects awaiting collection, producing false positives. [src7]

## Diagnostic Commands

> Full script: [diagnostic-commands.sh](scripts/diagnostic-commands.sh) (26 lines)

```bash
# Start app with inspector for Chrome DevTools
node --inspect app.js
# Start with increased heap (stopgap, not a fix)
node --max-old-space-size=4096 app.js
# Expose GC for manual triggering (debug only)
# ... (see full script)
```

## Version History & Compatibility

| Version | Memory Features | Key Changes |
|---|---|---|
| Node.js 24 LTS (Apr 2026) | V8 13.x | CppHeap GC improvements, smoother heap management under memory-constrained containers, more predictable tail latencies; some native-addon workloads see different allocation timing vs Node 22 — verify regressions with `heapUsed` before blaming app code [src8] |
| Node.js 22 LTS | V8 12.4 | `v8.queryObjects()` for counting live objects by prototype; V8 caches free pages instead of unmapping (higher RSS under sustained load, not a leak); stream.finished() perf regression fixed in 22.17.1 [src8] |
| Node.js 20 LTS | V8 11.3 | `v8.writeHeapSnapshot()` stable, `--diagnostic-dir` flag, `AbortSignal.timeout()` for timer cleanup, container-aware max heap size via cgroup limits [src1] |
| Node.js 18 LTS (EOL Apr 2025) | V8 10.2 | `v8.writeHeapSnapshot()` built-in, improved heap snapshot performance [src1] |
| Node.js 16 (EOL) | V8 9.4 | `WeakRef` and `FinalizationRegistry` stable [src1] |

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| `heapUsed` grows monotonically over time | High CPU but stable memory | CPU profiling (`clinic flame`, `0x`) |
| `MaxListenersExceededWarning` in logs | OOM kill on big data processing (expected) | Streaming / pagination |
| Process restarts due to FATAL ERROR heap | Slow response times without memory growth | Event loop profiling |
| Memory usage never decreases after load drops | Process uses lots of RSS but heapUsed is normal | V8 page caching (Node.js 22+) -- not a leak [src8] |
| Container OOM-kills despite reasonable heap limits | Memory spikes only during startup (one-time) | Increase container limit or `--max-old-space-size` |

## Important Caveats

- V8's GC is generational: short-lived objects in the "new space" are collected quickly, but long-lived objects promoted to "old space" require expensive mark-sweep cycles. A leak in old space grows slowly but steadily.
- `process.memoryUsage().external` tracks C++ objects bound to JavaScript (Buffers, native addons). A growing `external` value points to native memory leaks, not JavaScript heap leaks.
- `WeakRef` and `FinalizationRegistry` (Node.js 16+) are not replacements for proper cleanup. Weak references help with caches, but finalization timing is non-deterministic.
- In containerized environments (Docker, K8s), the OS may OOM-kill the process before V8's GC runs. Set `--max-old-space-size` to 75% of container memory limit to give GC headroom. Node.js 20+ reads cgroup limits automatically, but explicit flags provide tighter control. [src1]
- Cluster mode (`cluster` module) forks workers -- each has its own heap. A leak in one worker doesn't affect others, but all workers likely share the same leaky code.
- Node.js 22 changed V8's free-page strategy: instead of unmapping immediately, V8 caches free pages until idle periods or memory-reducing GC cycles. This causes RSS to appear higher under sustained CPU load. Introducing brief idle periods or applying memory pressure causes V8 to release cached pages. [src8]

## Related Units
<!-- Generated from related_kos frontmatter -->

- [Node.js ECONNREFUSED](/software/debugging/nodejs-econnrefused/2026)
- [Node.js Unhandled Promise Rejections](/software/debugging/nodejs-unhandled-promises/2026)
- [Redis Memory Issues](/software/debugging/redis-memory-issues/2026)
- [Docker OOMKilled](/software/debugging/docker-oomkilled/2026)
- [Python Memory Leaks](/software/debugging/python-memory-leaks/2026)
- [Go Goroutine Leak](/software/debugging/go-goroutine-leak/2026)
- [Java OutOfMemoryError](/software/debugging/java-outofmemoryerror/2026)
- [Next.js Build Failures](/software/debugging/nextjs-build-failures/2026)
