Fix ERR_WORKER_OUT_OF_MEMORY: Worker Thread Ran Out of Memory in Node.js

intermediate๐Ÿ’š Node.js2026-05-15| Node.js 12+ on Linux, macOS, Windows โ€” any system running Worker Threads with large datasets or memory-intensive operations

Error Message

Uncaught Error: ERR_WORKER_OUT_OF_MEMORY: Worker terminated due to reaching memory limit: JS heap could not be allocated
#worker-threads#memory#nodejs#multithreading#heap

TL;DR Quick Fix

Your Worker Thread hit the V8 heap limit โ€” 512 MB by default on 64-bit systems. The fastest fix is raising the heap when you spawn the worker:

const { Worker } = require('worker_threads');

const worker = new Worker('./my-worker.js', {
  resourceLimits: {
    maxOldGenerationSizeMb: 1024, // 1 GB
    maxYoungGenerationSizeMb: 128
  }
});

Still crashing, or memory keeps climbing after the fix? Keep reading. You probably have a leak inside the worker, and raising the limit just bought you extra time before the next crash.

What Triggers This Error

The full error looks like this:

Uncaught Error: ERR_WORKER_OUT_OF_MEMORY: Worker terminated due to reaching memory limit: JS heap could not be allocated

Worker Threads run in isolated V8 contexts with their own separate heap. When a worker tries to allocate more memory than its limit allows, V8 kills it immediately and throws this error back to the parent thread.

Here's what usually causes it:

  • Processing very large JSON files or datasets in one pass
  • Accumulating results in a growing array without streaming or batching
  • Memory leaks โ€” closures, event listeners, or cached objects that never get freed
  • Using SharedArrayBuffer or Buffer without releasing them
  • Recursive operations that build up a large call stack with retained references

Fix 1: Increase the Worker's Memory Limit

Pass resourceLimits when creating the Worker to raise the heap ceiling:

// parent.js
const { Worker } = require('worker_threads');

const worker = new Worker('./heavy-task.js', {
  resourceLimits: {
    maxOldGenerationSizeMb: 2048, // 2 GB for old gen heap
    maxYoungGenerationSizeMb: 256  // 256 MB for young gen
  }
});

worker.on('error', (err) => {
  console.error('Worker error:', err.message);
});

worker.on('exit', (code) => {
  if (code !== 0) console.error(`Worker stopped with exit code ${code}`);
});

Good fit for: tasks that genuinely need a large heap โ€” say, parsing a 500 MB JSON file entirely in memory. Memory usage is high but has a known ceiling.

Not a good fit for: memory that keeps climbing with no plateau. That's a leak. Raising the limit just delays the crash by a few minutes.

Fix 2: Stream or Batch Large Data Instead of Loading It All at Once

Loading an entire file into memory is the most common culprit. Switch to line-by-line streaming instead:

// heavy-task.js (worker)
const { parentPort } = require('worker_threads');
const fs = require('fs');
const readline = require('readline');

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

  let count = 0;
  for await (const line of rl) {
    // Process one line at a time โ€” no full-file array in memory
    count++;
  }

  parentPort.postMessage({ count });
}

processLargeFile('/data/large-log.txt');

For large arrays, process them in fixed-size batches and post intermediate results back to the parent. Don't collect everything first:

// Instead of:
const results = items.map(expensiveTransform); // holds everything in memory
parentPort.postMessage(results);

// Do this:
const BATCH_SIZE = 1000;
for (let i = 0; i = items.length });
  // Yield to GC before the next batch
  await new Promise(resolve => setImmediate(resolve));
}

Fix 3: Find and Fix Memory Leaks Inside the Worker

Memory growing without a ceiling means a leak. Drop a periodic check at the top of your worker to catch it early:

// At the top of your worker file
const CHECK_INTERVAL_MS = 5000;
setInterval(() => {
  const { heapUsed, heapTotal } = process.memoryUsage();
  console.log(`[Worker] Heap: ${Math.round(heapUsed / 1024 / 1024)} MB / ${Math.round(heapTotal / 1024 / 1024)} MB`);
}, CHECK_INTERVAL_MS).unref(); // .unref() so this doesn't block worker exit

Four patterns account for most worker leaks:

  • Event listeners not removed: Call emitter.removeAllListeners() when done, or use once() instead of on() for one-time handlers.
  • Unbounded caches: A Map or plain object used as a cache will grow forever unless entries are evicted. Consider an LRU cache library with a fixed size limit.
  • Closures holding references: Callbacks inside loops can silently hold onto large arrays via closure. Read them carefully.
  • Timers not cleared: setInterval keeps a reference to its callback and everything it closes over. Clear intervals when the work is done.
// Leak: 'log' grows by one entry every 100ms and never shrinks
let log = [];
const interval = setInterval(() => {
  log.push(Date.now());
}, 100);

// Fix: clear the interval and drop the reference when you're done
clearInterval(interval);
log = null;

Fix 4: Use --max-old-space-size for the Whole Process

When you control how Node.js launches and every worker needs more headroom, set the V8 flag at the process level:

node --max-old-space-size=4096 parent.js

This sets a 4 GB old-generation heap for the entire Node.js process, including any workers that don't specify their own resourceLimits. It's a quick ceiling raise that covers everything at once. For production, prefer per-worker resourceLimits โ€” it gives you precise control and prevents a runaway worker from consuming memory meant for others.

Verify the Fix Worked

Wire up a message handler that confirms the worker exited cleanly and reports its final heap usage:

// parent.js
worker.on('message', (msg) => {
  if (msg.done) {
    console.log('Worker completed successfully. Heap at exit:', msg.heapUsed);
  }
});

worker.on('error', (err) => {
  // ERR_WORKER_OUT_OF_MEMORY should no longer appear here
  console.error('Worker failed:', err.code, err.message);
});
// worker.js โ€” report heap usage on completion
const { heapUsed } = process.memoryUsage();
parentPort.postMessage({ done: true, heapUsed });

Watch resident memory while the task runs:

# Monitor RSS of the node process in real time
node --expose-gc parent.js &
watch -n 1 "ps -o pid,rss,vsz,comm -p $(pgrep -n node)"

A clean exit with code 0 and no ERR_WORKER_OUT_OF_MEMORY in the error handler means it worked. RSS still climbing to the new ceiling? Go back to Fix 2 and Fix 3 โ€” the limit increase helped, but the underlying problem is still there.

Quick Reference

  • One-time large task, known size: Raise maxOldGenerationSizeMb in resourceLimits
  • Processing large files: Use streams + readline, never load the whole file into an array
  • Memory grows without bound: You have a leak โ€” audit listeners, caches, and timers
  • All workers need more memory: Use --max-old-space-size at launch

Related Error Notes