- Published on
Understanding JavaScript Heap out-of-memory errors in Node.js
Introduction
JavaScript heap out-of-memory errors in Node.js can be a common source of frustration for developers. These errors often occur unexpectedly, leading to application crashes and disrupted user experiences. Understanding the root causes of these errors and how to resolve them is crucial for maintaining the stability and performance of your Node.js applications.
Memory management in Node.js, driven by the V8 JavaScript engine, involves complex processes like garbage collection. When these processes are unable to reclaim enough memory, your application may encounter out-of-memory errors. Let's explore how to identify, diagnose, and fix these issues effectively.
Note: For this post, I'm using NodeJS v20.13.1.
Table of Contents
- Introduction
- What is a JavaScript Heap out-of-memory Error?
- How to print current max-old-space-size
- Analyzing the Stack Trace
- Causes of Heap out-of-memory Errors
- Solutions to Prevent and Resolve Memory Errors
- Example: Handling Large Data Sets with Streams
- Breakdown of a real-world stack trace in Node.js
- Conclusion
What is a JavaScript Heap out-of-memory Error?
A JavaScript heap out-of-memory error occurs when the Node.js process exceeds the allocated memory limit. This is managed by the V8 engine, which is responsible for executing JavaScript code and managing memory allocation. The heap is the memory space where objects, arrays, and other data structures are stored.
Understanding Garbage Collection
Garbage collection is the process of reclaiming memory occupied by objects that are no longer in use. In Node.js, this is handled by the V8 engine. The V8 engine uses two types of garbage collection:
- Scavenge (Minor GC): Quickly cleans up the young generation heap space.
- Mark-Sweep-Compact (Major GC): Thoroughly cleans up the old generation heap space.
When these processes fail to reclaim enough memory, you encounter allocation failures, leading to heap out-of-memory errors.
How to print current max-old-space-size
NodeJS gives us an option to view the current max-old-space-size
, which is the maximum amount of memory allocated for the V8 heap. You can use the --max-old-space-size
flag to set the max-old-space-size.
To get the current max-old-space-size, run the following oneliner:
# The `-e` runs NodeJS and evaluates the preceeding string as JavaScript
node -e "console.log(Math.round(require('v8').getHeapStatistics().heap_size_limit / (1024 * 1024)) + ' MB')" # 4144 MB
If you want to get the value in your code, you can use the following code:
import v8 from 'v8'
const heapStats = v8.getHeapStatistics()
// in MB
const maxOldSpaceSizeMB = Math.round(heapStats.heap_size_limit / (1024 * 1024))
console.log(`Current max-old-space-size: ${maxOldSpaceSizeMB} MB`)
To start nodejs with a specific max-old-space-size
, run the following command when starting your server:
node --max-old-space-size=4096 server.js # 4096 = 4GB
Analyzing the Stack Trace
Here's an example of a stack trace from a Node.js application that encountered a heap out-of-memory error:
<--- Last few GCs --->
[97355:0x140008000] 50369 ms: Scavenge 3348.8 (4127.0) -> 3334.2 (4127.8) MB, 2.38 / 0.00 ms (average mu = 0.267, current mu = 0.084) allocation failure;
[97355:0x140008000] 50374 ms: Scavenge 3347.5 (4127.8) -> 3334.8 (4128.0) MB, 1.75 / 0.00 ms (average mu = 0.267, current mu = 0.084) task;
[97355:0x140008000] 50381 ms: Scavenge 3349.7 (4128.0) -> 3342.6 (4128.5) MB, 4.46 / 0.00 ms (average mu = 0.267, current mu = 0.084) allocation failure;
<--- JS stacktrace --->
FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out-of-memory
Key Elements of the Stack Trace
- Scavenge Events: Indicate minor garbage collection attempts.
- Allocation Failure: Shows that memory allocation attempts failed.
- Mark-Compact: Indicates a major garbage collection event that was ineffective.
Causes of Heap out-of-memory Errors
Several factors can lead to these errors in Node.js applications:
- Memory Leaks: Objects are retained in memory unintentionally, consuming heap space over time.
- Large Data Sets: Processing large data sets without streaming can quickly exhaust available memory.
- Improper Configuration: Default memory limits for Node.js might be insufficient for your application's needs.
Solutions to Prevent and Resolve Memory Errors
Increasing Memory Limits
By default, Node.js allocates a limited amount of memory. You can increase this limit using the --max-old-space-size
flag:
Read: https://nodejs.org/api/cli.html#--max-old-space-sizesize-in-megabytes
node --max-old-space-size=4096 yourScript.js
This example allocates 4GB
of memory to the Node.js process.
Identifying Memory Leaks
Memory leaks are a common cause of heap out-of-memory errors. Tools like Chrome DevTools and Node.js's built-in profiler can help identify leaks:
Chrome DevTools: Use the Memory tab to take heap snapshots and compare memory usage over time.
Node.js Profiler: Use
node --inspect yourScript.js
to debug memory issues.
Optimizing Code
Efficient memory usage can prevent out-of-memory errors:
Use Streams: For processing large data sets, use streams to handle data in chunks instead of loading it all into memory.
Avoid Global Variables: Global variables can lead to memory leaks if not managed properly.
Garbage Collection Tuning
You can tweak garbage collection settings to improve performance:
- Manual Garbage Collection: Expose garbage collection using
--expose-gc
and callglobal.gc()
manually in specific parts of your application.
// start the nodejs process with --expose-gc flag
// > node --expose-gc yourScript.js
if (global.gc) {
global.gc() // <- manually trigger garbage collection
} else {
console.log(`GC is not enabled. Pass --expose-gc when launching Node.js`)
}
- Heap Profiling: Use tools like heapdump to analyze memory usage and identify problematic areas.
Keeping Dependencies Up-to-Date
Outdated dependencies can introduce memory leaks. Regularly update your dependencies and monitor their performance.
Example: Handling Large Data Sets with Streams
Suppose you are processing a large file. Instead of reading the entire file into memory, use streams
:
Node.js fs module: https://nodejs.org/api/fs.html#filehandlecreatereadstreamoptions
import fs from 'fs'
import readline from 'readline'
const processFileByPath = (filePath: string): void => {
const readStream = fs.createReadStream(filePath)
const rl = readline.createInterface({
input: readStream,
})
rl.on('line', (line: string) => {
// do something with each line
console.log(line)
})
rl.on('close', () => {
console.log('file processing completed.')
})
rl.on('error', (error: Error) => {
console.error(`Error processing the file: ${error.message}`, error)
})
}
// start processing the file
processFileByPath('largeTextFile.txt')
This approach ensures that only a small portion of the file is kept in memory at any time.
Breakdown of a real-world stack trace in Node.js
This is a real stack trace from a Node.js application that I deliberately caused to run out-of-memory. The application was a simple script that read a large file into memory and processed it line by line. The file was large enough to cause the process to exceed the memory limit.
<--- Last few GCs --->
[97355:0x140008000] 50369 ms: Scavenge 3348.8 (4127.0) -> 3334.2 (4127.8) MB, 2.38 / 0.00 ms (average mu = 0.267, current mu = 0.084) allocation failure;
[97355:0x140008000] 50374 ms: Scavenge 3347.5 (4127.8) -> 3334.8 (4128.0) MB, 1.75 / 0.00 ms (average mu = 0.267, current mu = 0.084) task;
[97355:0x140008000] 50381 ms: Scavenge 3349.7 (4128.0) -> 3342.6 (4128.5) MB, 4.46 / 0.00 ms (average mu = 0.267, current mu = 0.084) allocation failure;
<--- JS stacktrace --->
FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out-of-memory
----- Native stack trace -----
1: 0x104e18b08 node::OOMErrorHandler(char const*, v8::OOMDetails const&) [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
2: 0x104f9fe30 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, v8::OOMDetails const&) [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
3: 0x105174504 v8::internal::Heap::GarbageCollectionReasonToString(v8::internal::GarbageCollectionReason) [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
4: 0x1051783b8 v8::internal::Heap::CollectGarbageShared(v8::internal::LocalHeap*, v8::internal::GarbageCollectionReason) [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
5: 0x105174e1c v8::internal::Heap::PerformGarbageCollection(v8::internal::GarbageCollector, v8::internal::GarbageCollectionReason, char const*) [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
6: 0x105172ba4 v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
7: 0x1051697f8 v8::internal::HeapAllocator::AllocateRawWithLightRetrySlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
8: 0x10516a058 v8::internal::HeapAllocator::AllocateRawWithRetryOrFailSlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
9: 0x10514e98c v8::internal::Factory::AllocateRaw(int, v8::internal::AllocationType, v8::internal::AllocationAlignment) [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
10: 0x1051450f0 v8::internal::MaybeHandle<v8::internal::SeqTwoByteString> v8::internal::FactoryBase<v8::internal::Factory>::NewRawStringWithMap<v8::internal::SeqTwoByteString>(int, v8::internal::Map, v8::internal::AllocationType) [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
11: 0x1054543f8 v8::internal::String::SlowFlatten(v8::internal::Isolate*, v8::internal::Handle<v8::internal::ConsString>, v8::internal::AllocationType) [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
12: 0x105546028 v8::internal::Runtime_StringSplit(int, unsigned long*, v8::internal::Isolate*) [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
13: 0x105894c44 Builtins_CEntry_Return1_ArgvOnStack_NoBuiltinExit [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
14: 0x1058861bc Builtins_StringPrototypeSplit [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
15: 0x10b0d1b8c
16: 0x10b0f62cc
17: 0x10abf7c84
18: 0x10adb5948
19: 0x10b0d15d0
20: 0x10afccdb4
21: 0x10b00fd7c
22: 0x10ad9bb58
23: 0x10b0cba68
24: 0x10580a50c Builtins_JSEntryTrampoline [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
25: 0x10580a1f4 Builtins_JSEntry [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
26: 0x1050e19ac v8::internal::(anonymous namespace)::Invoke(v8::internal::Isolate*, v8::internal::(anonymous namespace)::InvokeParams const&) [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
27: 0x1050e0df8 v8::internal::Execution::Call(v8::internal::Isolate*, v8::internal::Handle<v8::internal::Object>, v8::internal::Handle<v8::internal::Object>, int, v8::internal::Handle<v8::internal::Object>*) [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
28: 0x104fbb6e8 v8::Function::Call(v8::Local<v8::Context>, v8::Local<v8::Value>, int, v8::Local<v8::Value>*) [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
29: 0x104d44fa0 node::InternalMakeCallback(node::Environment*, v8::Local<v8::Object>, v8::Local<v8::Object>, v8::Local<v8::Function>, int, v8::Local<v8::Value>*, node::async_context) [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
30: 0x104d452b8 node::MakeCallback(v8::Isolate*, v8::Local<v8::Object>, v8::Local<v8::Function>, int, v8::Local<v8::Value>*, node::async_context) [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
31: 0x104dba464 node::Environment::CheckImmediate(uv_check_s*) [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
32: 0x1057f1f6c uv__run_check [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
33: 0x1057ebc8c uv_run [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
34: 0x104d456f0 node::SpinEventLoopInternal(node::Environment*) [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
35: 0x104e582e4 node::NodeMainInstance::Run(node::ExitCode*, node::Environment*) [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
36: 0x104e57ff8 node::NodeMainInstance::Run() [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
37: 0x104de07ac node::Start(int, char**) [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
38: 0x19d0b20e0 start [/usr/lib/dyld]
Quite a bit big wall of text, right? Let's break it down:
Breakdown of Key Elements
Garbage Collection Logs
The initial part of the trace provides details about recent garbage collection (GC) activities:
<--- Last few GCs --->
[97355:0x140008000] 50369 ms: Scavenge 3348.8 (4127.0) -> 3334.2 (4127.8) MB, 2.38 / 0.00 ms (average mu = 0.267, current mu = 0.084) allocation failure;
[97355:0x140008000] 50374 ms: Scavenge 3347.5 (4127.8) -> 3334.8 (4128.0) MB, 1.75 / 0.00 ms (average mu = 0.267, current mu = 0.084) task;
[97355:0x140008000] 50381 ms: Scavenge 3349.7 (4128.0) -> 3342.6 (4128.5) MB, 4.46 / 0.00 ms (average mu = 0.267, current mu = 0.084) allocation failure;
Scavenge Events: These lines indicate scavenging operations, which are minor garbage collection activities focused on the young generation heap space.
Memory Values: The numbers
3348.8 (4127.0)
->3334.2 (4127.8) MB
show memory usage before and after the GC operation.Timing: The times
2.38 / 0.00 ms
show the duration of the GC operation.Mu (Utilization):
average mu = 0.267
,current mu = 0.084
indicates the memory utilization, showing how efficiently the memory is being used.Allocation Failure: Indicates that memory allocation failed despite the GC attempts.
Fatal Error Description
<--- JS stacktrace --->
FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out-of-memory
This message indicates that the process ran out-of-memory. The real details are in the stack-trace that follows.
Detailed Native Stack Trace
The stack trace provides insights into the internal workings of Node.js and V8
at the point of failure:
1: 0x104e18b08 node::OOMErrorHandler(char const*, v8::OOMDetails const&) [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
2: 0x104f9fe30 v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, v8::OOMDetails const&) [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
3: 0x105174504 v8::internal::Heap::GarbageCollectionReasonToString(v8::internal::GarbageCollectionReason) [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
4: 0x1051783b8 v8::internal::Heap::CollectGarbageShared(v8::internal::LocalHeap*, v8::internal::GarbageCollectionReason) [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
5: 0x105174e1c v8::internal::Heap::PerformGarbageCollection(v8::internal::GarbageCollector, v8::internal::GarbageCollectionReason, char const*) [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
6: 0x105172ba4 v8::internal::Heap::CollectGarbage(v8::internal::AllocationSpace, v8::internal::GarbageCollectionReason, v8::GCCallbackFlags) [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
7: 0x1051697f8 v8::internal::HeapAllocator::AllocateRawWithLightRetrySlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
8: 0x10516a058 v8::internal::HeapAllocator::AllocateRawWithRetryOrFailSlowPath(int, v8::internal::AllocationType, v8::internal::AllocationOrigin, v8::internal::AllocationAlignment) [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
9: 0x10514e98c v8::internal::Factory::AllocateRaw(int, v8::internal::AllocationType, v8::internal::AllocationAlignment) [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
10: 0x1051450f0 v8::internal::MaybeHandle<v8::internal::SeqTwoByteString> v8::internal::FactoryBase<v8::internal::Factory>::NewRawStringWithMap<v8::internal::SeqTwoByteString>(int, v8::internal::Map, v8::internal::AllocationType) [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
11: 0x1054543f8 v8::internal::String::SlowFlatten(v8::internal::Isolate*, v8::internal::Handle<v8::internal::ConsString>, v8::internal::AllocationType) [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
12: 0x105546028 v8::internal::Runtime_StringSplit(int, unsigned long*, v8::internal::Isolate*) [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
13: 0x105894c44 Builtins_CEntry_Return1_ArgvOnStack_NoBuiltinExit [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
14: 0x1058861bc Builtins_StringPrototypeSplit [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
15: 0x10b0d1b8c
16: 0x10b0f62cc
17: 0x10abf7c84
18: 0x10adb5948
19: 0x10b0d15d0
20: 0x10afccdb4
21: 0x10b00fd7c
22: 0x10ad9bb58
23: 0x10b0cba68
24: 0x10580a50c Builtins_JSEntryTrampoline [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
25: 0x10580a1f4 Builtins_JSEntry [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
26: 0x1050e19ac v8::internal::(anonymous namespace)::Invoke(v8::internal::Isolate*, v8::internal::(anonymous namespace)::InvokeParams const&) [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
27: 0x1050e0df8 v8::internal::Execution::Call(v8::internal::Isolate*, v8::internal::Handle<v8::internal::Object>, v8::internal::Handle<v8::internal::Object>, int, v8::internal::Handle<v8::internal::Object>*) [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
28: 0x104fbb6e8 v8::Function::Call(v8::Local<v8::Context>, v8::Local<v8::Value>, int, v8::Local<v8::Value>*) [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
29: 0x104d44fa0 node::InternalMakeCallback(node::Environment*, v8::Local<v8::Object>, v8::Local<v8::Object>, v8::Local<v8::Function>, int, v8::Local<v8::Value>*, node::async_context) [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
30: 0x104d452b8 node::MakeCallback(v8::Isolate*, v8::Local<v8::Object>, v8::Local<v8::Function>, int, v8::Local<v8::Value>*, node::async_context) [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
31: 0x104dba464 node::Environment::CheckImmediate(uv_check_s*) [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
32: 0x1057f1f6c uv__run_check [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
33: 0x1057ebc8c uv_run [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
34: 0x104d456f0 node::SpinEventLoopInternal(node::Environment*) [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
35: 0x104e582e4 node::NodeMainInstance::Run(node::ExitCode*, node::Environment*) [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
36: 0x104e57ff8 node::NodeMainInstance::Run() [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
37: 0x104de07ac node::Start(int, char**) [/Users/utkarshbhatt/.nvm/versions/node/v20.13.1/bin/node]
38: 0x19d0b20e0 start [/usr/lib/dyld]
Node.js and V8 Error Handling: Functions like
node::OOMErrorHandler
andv8::internal::V8::FatalProcessOutOfMemory
are invoked when Node.js or V8 encounters anout-of-memory
error. These functions log the error and initiate the process shutdown.Garbage Collection: Functions such as
v8::internal::Heap::CollectGarbage
andv8::internal::Heap::PerformGarbageCollection
indicate the V8 engine's efforts to free up memory. Despite these attempts, the heap was too full to accommodate additional allocations.Memory Allocation: Functions like
v8::internal::HeapAllocator::AllocateRawWithLightRetrySlowPath
andv8::internal::Factory::AllocateRaw
are involved in memory allocation. The allocation failed, causing the process to crash.Application Logic: The stack trace also shows functions related to the application's execution, such as
node::MakeCallback
anduv_run
. These functions indicate the general flow of the application before the crash occurred.
Conclusion
By increasing memory limits, identifying memory leaks, optimizing code, tuning garbage collection, and keeping dependencies up-to-date, you can prevent and fix these errors.
One more thing. All this testing/coding was performed on a M3 Macbook Pro with 18GB of RAM. You might run into these limits early on a machine with lesser memory.