Logo
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.

nodejs out of memory

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.

Table of Contents

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:

  1. Scavenge (Minor GC): Quickly cleans up the young generation heap space.
  2. 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.

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:

  1. Memory Leaks: Objects are retained in memory unintentionally, consuming heap space over time.
  2. Large Data Sets: Processing large data sets without streaming can quickly exhaust available memory.
  3. 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 call global.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 and v8::internal::V8::FatalProcessOutOfMemory are invoked when Node.js or V8 encounters an out-of-memory error. These functions log the error and initiate the process shutdown.

  • Garbage Collection: Functions such as v8::internal::Heap::CollectGarbage and v8::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 and v8::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 and uv_run. These functions indicate the general flow of the application before the crash occurred.

Conclusion

JavaScript heap out-of-memory errors in Node.js can be challenging, but understanding their causes and solutions is key to resolving them.

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.