Learn How to Fix "JavaScript Heap Out of Memory" in Node.js

Muhaymin Bin Mehmood

Muhaymin Bin Mehmood

·
10 min read
Published Sep 23, 2024
Updated Oct 8, 2026
Learn How to Fix JavaScript Heap Out of Memory in Node.js
18 min read
1,513 words
28 sections18 code blocks

"JavaScript heap out of memory" means V8, the engine inside Node.js, couldn't free enough memory to keep allocating JavaScript objects, so it stopped the process. There are two kinds of fixes: give Node a bigger heap, or make your code use less. The first takes one line; the second is what stops it coming back.

Quick fix: raise the heap limit. For a script:

Bash
node --max-old-space-size=8192 app.js

For npm run build, next build, webpack, Angular and other tools you don't start with node yourself, set NODE_OPTIONS instead (details below):

Bash
NODE_OPTIONS="--max-old-space-size=8192" npm run build

Tested on: Node.js 20.20.2, Windows, 16 GB RAM, October 2026. Every cause below was reproduced with a small heap limit and every fix measured.

What the Error Looks Like

This is the real output from a script that ran out of heap:

Bash
<--- Last few GCs --->

[34392:0000018F30823C50]      247 ms: Mark-Compact 68.7 (86.7) -> 65.1 (100.4) MB, 88.71 / 0.00 ms  (average mu = 0.327, current mu = 0.088) allocation failure; scavenge might not succeed

<--- JS stacktrace --->

FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
----- Native stack trace -----

 1: 00007FF6A04EC23B node::SetCppgcReference+19883
 2: 00007FF6A045B1B8 v8::base::CPU::num_virtual_address_bits+93912
 ...

You'll see one of two lines, depending on how the last garbage collections went:

  • FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory
  • FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory

Both mean the same thing. The "Last few GCs" block shows the heap barely shrinking after each full collection — the memory is still in use, so V8 gives up.

Quick Fix: Raise the Heap Limit

Check your current limit first

Bash
node -e "console.log(require('v8').getHeapStatistics().heap_size_limit / 1024 / 1024)"

The default isn't a fixed 1 GB or 2 GB: it depends on the Node version and how much memory the machine has. On our 16 GB machine Node 20 already allowed about 4,144 MB, so --max-old-space-size=4096 changed nothing. Pick a value above your current limit, and leave room for everything else: the heap is only part of the process's memory (rss), and the operating system needs the rest. In a container, keep it well below the container's memory limit, or the container gets killed before Node can report anything.

For a script you start with node

Bash
node --max-old-space-size=8192 app.js

The value is in megabytes. --max-old-space-size sets the size of V8's "old space", where long-lived objects live — in practice, the heap limit.

For npm scripts and build tools (NODE_OPTIONS)

When the crash happens inside npm run build or a CLI like next build, there's no node command to add the flag to. Node reads extra flags from the NODE_OPTIONS environment variable instead. We confirmed each of these raised the limit:

Bash
# macOS / Linux / Git Bash
NODE_OPTIONS="--max-old-space-size=8192" npm run build

# Windows PowerShell
$env:NODE_OPTIONS="--max-old-space-size=8192"; npm run build

# Windows Command Prompt
set NODE_OPTIONS=--max-old-space-size=8192 && npm run build

To make it part of the project rather than your terminal, use cross-env (npm install --save-dev cross-env) so the same script works on Windows, macOS and Linux:

JSON
{
  "scripts": {
    "build": "cross-env NODE_OPTIONS=--max-old-space-size=8192 next build"
  }
}

In CI (GitHub Actions, Vercel, Netlify) set NODE_OPTIONS as an environment variable in the project settings or workflow file.

Why Raising the Limit Often Isn't Enough

A bigger heap fixes a process that genuinely needs more memory — a large build, a one-off data import. If memory keeps growing (a leak, an unbounded cache, a file read whole), a bigger heap only delays the crash and can make the machine swap. If raising the limit doesn't fix it, or the crash comes back later, find what is using the memory.

Find What Is Using the Memory

1. Log memory usage

JavaScript
function logMemory(label) {
  const { heapUsed, heapTotal, rss, external } = process.memoryUsage();
  const mb = (bytes) => `${Math.round(bytes / 1024 / 1024)} MB`;
  console.log(label, {
    heapUsed: mb(heapUsed),
    heapTotal: mb(heapTotal),
    rss: mb(rss),
    external: mb(external),
  });
}

// Log every 10 seconds without keeping the process alive
setInterval(() => logMemory("memory"), 10_000).unref();

heapUsed growing steadily over time while the workload stays the same is the classic sign of a leak. rss is the whole process; external is memory held outside the heap, such as Buffers.

2. Write a heap snapshot just before the crash

Bash
node --heapsnapshot-near-heap-limit=1 app.js

When the heap is about to run out, Node writes a .heapsnapshot file in the working directory and then crashes as usual. Open Chrome DevTools, go to the Memory tab, load the file, and sort by Retained Size. In our test, about 121 MB of the 128 MB heap was arrays held by the leaking cache, which made the culprit obvious. The file was 480 MB, though, so make sure there's disk space.

3. Inspect a running process

Bash
node --inspect app.js

Open chrome://inspect in Chrome, click Inspect under your process, and take two heap snapshots a minute apart in the Memory tab. Switching the second snapshot to the Comparison view shows which objects were created and never freed.

4. Watch garbage collection

Bash
node --trace-gc app.js

Each line shows the heap size before and after a collection. If "after" keeps climbing, something is holding on to memory.

Common Causes and How to Fix Them

1. Reading a whole file into memory

JavaScript
const fs = require("node:fs");

const lines = fs.readFileSync("big.ndjson", "utf8").split("\n");
let total = 0;
for (const line of lines) {
  if (line) total += JSON.parse(line).amount;
}
console.log("total:", total.toFixed(1));

For a 96 MB file this needed about 145 MB of heap, and crashed with --max-old-space-size=128. Reading it line by line with a stream keeps only the current line in memory:

JavaScript
const fs = require("node:fs");
const readline = require("node:readline");

async function sumAmounts(path) {
  const rl = readline.createInterface({
    input: fs.createReadStream(path),
    crlfDelay: Infinity,
  });

  let total = 0;
  for await (const line of rl) {
    if (line) total += JSON.parse(line).amount;
  }
  return total;
}

sumAmounts("big.ndjson").then((total) => console.log("total:", total.toFixed(1)));

The streaming version produced the same total with the heap capped at just 32 MB (peak about 20 MB). The same idea applies to large HTTP responses, database exports and CSV files: process them as streams rather than loading them whole.

2. A cache or array that only grows

JavaScript
// A cache that only grows: every new key stays in memory forever
const cache = new Map();

function getReport(userId) {
  if (!cache.has(userId)) {
    cache.set(userId, { userId, rows: new Array(1000).fill(userId) });
  }
  return cache.get(userId);
}

Every new key adds an entry that is never removed. With a 128 MB heap this crashed before 50,000 users. Give the cache a size limit:

JavaScript
// Keep at most MAX_ENTRIES; a Map remembers insertion order,
// so the first key is the least recently used one
const MAX_ENTRIES = 1000;
const cache = new Map();

function getReport(userId) {
  if (cache.has(userId)) {
    const value = cache.get(userId);
    cache.delete(userId); // move to the end: most recently used
    cache.set(userId, value);
    return value;
  }
  const value = { userId, rows: new Array(1000).fill(userId) };
  cache.set(userId, value);
  if (cache.size > MAX_ENTRIES) {
    cache.delete(cache.keys().next().value); // evict the oldest
  }
  return value;
}

After 200,000 lookups the cache held exactly 1,000 entries and the heap stayed around 20–40 MB. The same applies to arrays of logs, event listeners added on every request, and objects stored on module-level variables: anything that lives as long as the process needs a limit or an expiry. For production, a library such as lru-cache adds TTLs and size-based limits.

3. Starting thousands of async tasks at once

Promise.all(items.map(handle)) starts every task immediately, so all of their in-progress data is in memory at the same time. Processing in batches caps that:

JavaScript
async function processInBatches(items, batchSize, worker) {
  for (let i = 0; i < items.length; i += batchSize) {
    const batch = items.slice(i, i + batchSize);
    await Promise.all(batch.map(worker));
  }
}

With 20,000 tasks that each held about 48 KB while running, starting them all at once peaked at 937 MB of heap and crashed with a 256 MB limit. Batches of 100 peaked at 25 MB. Batching also keeps you from overwhelming databases and APIs with 20,000 simultaneous requests.

4. Large builds (Next.js, webpack, Angular, TypeScript)

Build tools load your whole dependency graph, source maps and type information into one process. Raising the limit with NODE_OPTIONS is usually the right fix here. If builds keep growing, also check for very large files being imported into the bundle (data dumps, icon sets imported in full) and update the build tool — memory use often improves between major versions.

Isolate Memory-Hungry Jobs in a Worker

Worker threads don't reduce memory — each worker has its own V8 heap, so total memory goes up. What they give you is a separate limit: if a worker runs out of memory, only the worker dies, and your main process (say, a web server) keeps running.

JavaScript
const { Worker } = require("node:worker_threads");

function runJob(file, data) {
  return new Promise((resolve, reject) => {
    const worker = new Worker(file, {
      workerData: data,
      resourceLimits: { maxOldGenerationSizeMb: 256 }, // this worker's heap limit
    });
    worker.once("message", resolve);
    worker.once("error", reject); // ERR_WORKER_OUT_OF_MEMORY if it runs out
    worker.once("exit", (code) => {
      if (code !== 0) reject(new Error(`Worker stopped with exit code ${code}`));
    });
  });
}

runJob("./job.js", { rows: 50_000_000 })
  .then((result) => console.log("Result:", result))
  .catch((err) => console.error("Job failed:", err.code || err.message));

// The main process keeps serving requests
setTimeout(() => console.log("Main process still running"), 3000);
JavaScript
const { parentPort, workerData } = require("node:worker_threads");

const rows = [];
for (let i = 0; i < workerData.rows; i++) {
  rows.push({ id: i, total: i * 2 });
}
parentPort.postMessage(rows.length);

In our test the 50-million-row job hit its 256 MB limit, the promise rejected with ERR_WORKER_OUT_OF_MEMORY, and the main process carried on and printed "Main process still running". The same job with 100,000 rows completed normally. See the resourceLimits documentation for the other limits you can set.

Quick Fix Checklist

SituationWhat to do
Crash in a script you run with nodenode --max-old-space-size=8192 app.js
Crash in npm run build / next build / CISet NODE_OPTIONS=--max-old-space-size=8192 (cross-env in package.json)
Raising the limit doesn't helpLog memory, take a heap snapshot, look for what keeps growing
Large files or responsesStream them instead of reading them whole
Cache, array or Map that only growsCap its size (LRU) or expire entries
Promise.all over thousands of itemsProcess in batches
One heavy job can take down the serverRun it in a worker with resourceLimits

FAQs

What causes "JavaScript heap out of memory" in Node.js?

The process needs more memory for JavaScript objects than V8's heap limit allows, and garbage collection can't free enough. That's either a workload that genuinely needs more (a big build or import) or code that keeps memory it no longer needs.

How do I increase the memory limit in Node.js?

Use node --max-old-space-size=8192 app.js for scripts, or the NODE_OPTIONS environment variable for npm scripts and build tools. The value is in megabytes.

What is the default heap size in Node.js?

It depends on the Node version and the machine's memory, so check it with v8.getHeapStatistics().heap_size_limit. On our 16 GB machine Node 20 defaulted to about 4 GB.

How much should I set --max-old-space-size to?

Higher than your current limit, and comfortably below the memory actually available. Leave room for the rest of the process and the operating system, and in containers stay well under the container's limit.

Why does the error still happen after increasing the limit?

Either the new limit is no higher than the old default (common with 4096 on machines with 16 GB or more), the flag isn't reaching the process (use NODE_OPTIONS for npm scripts), or memory keeps growing because of a leak.

How do I find a memory leak in Node.js?

Log process.memoryUsage() to confirm memory grows, then compare two heap snapshots (--inspect with Chrome DevTools) or capture one automatically with --heapsnapshot-near-heap-limit=1.

Does upgrading Node.js fix it?

Sometimes it helps — newer versions improve memory use and garbage collection — but it won't fix a leak or a file being read whole. Treat it as a bonus, not the fix.

Do worker threads reduce memory usage?

No. Each worker has its own heap, so total memory goes up. Use them to isolate heavy jobs with their own resourceLimits, not to save memory.

No comments yet. Be the first to comment!

Leave a Comment

2000 characters remaining

Related Articles

View all
Muhaymin Bin Mehmood

About Muhaymin Bin Mehmood

Front-end Developer skilled in the MERN stack, experienced in web and mobile development. Proficient in React.js, Node.js, and Express.js, with a focus on client interactions, sales support, and high-performance applications.