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
SharedArrayBufferorBufferwithout 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 useonce()instead ofon()for one-time handlers. - Unbounded caches: A
Mapor 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:
setIntervalkeeps 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
maxOldGenerationSizeMbinresourceLimits - 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-sizeat launch

