Skip to content

Fix: FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory

FixDevs · (Updated: )

Part of:  JavaScript & TypeScript Errors

Quick Answer

How to fix the JavaScript heap out of memory error by increasing Node.js memory limits, fixing memory leaks, and optimizing builds in webpack, Vite, and Docker.

V8 Hit Its Heap Ceiling

Personally, this is the Node.js error I have escalated the most often during pre-launch crunches: a green build pipeline that worked on a developer laptop crashes in CI because the container only has 2 GB. The first instinct (“just bump --max-old-space-size”) fixes 60 percent of cases and hides the leak in the other 40. I have learned to check whether memory is genuinely needed before adding more. You run a Node.js script, start a build, or launch your development server, and the process crashes with this:

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

 1: 0x100a7a1c4 node::OOMErrorHandler(char const*, v8::OOMDetails const&)
 2: 0x100c1e5d0 v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, v8::OOMDetails const&)
 3: 0x100c1e56c v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, v8::OOMDetails const&)
 4: 0x100dfba60 v8::internal::Heap::GarbageCollectionReasonToString(v8::internal::GarbageCollectionReason)

Sometimes it shows up during npm run build, webpack compilation, or when processing large datasets. The error might also appear as:

FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory

Or:

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

Regardless of the exact variant, the root cause is the same: Node.js ran out of memory.

Quick Reference Before You Dive In

If you arrived here from Google with a fresh OOM crash, the five facts that resolve roughly 90 percent of cases:

  1. --max-old-space-size=NNNN (megabytes) raises V8’s old-generation limit. On modern 64-bit Node the default is around 4 GB; set higher when you genuinely need it. The Node.js CLI documentation and the V8 garbage collector docs are the canonical references.
  2. NODE_OPTIONS="--max-old-space-size=4096" propagates to every Node invocation. Set it once in your environment or in package.json scripts via cross-env for cross-platform compatibility.
  3. In Docker / CI, raise the CONTAINER memory FIRST, then raise V8’s limit. A V8 limit higher than the container’s allocation triggers the kernel OOM killer before V8 can recover. Set V8 to roughly 75 percent of container memory.
  4. JSON.parse(fs.readFileSync(...)) on a large file uses ~2x the file size in heap. A 500 MB JSON file needs 1+ GB. Stream with stream-json or csv-parser instead of reading whole files.
  5. If memory grows over time, you have a LEAK, not a sizing problem. Take heap snapshots with --inspect and compare to find the retainer tree. Increasing the limit only delays the crash.

The rest of this article walks through each cause in detail, plus the failure modes most other guides skip.

How V8 Manages the Heap and Why It Fills Up

Node.js uses V8 as its JavaScript engine, and V8 has a default memory limit. On 64-bit systems, this limit is approximately 1.5 GB to 2 GB depending on the Node.js version. When your process tries to allocate memory beyond this ceiling, V8’s garbage collector cannot free enough space, and the process crashes.

Several common scenarios trigger this:

  • Large builds: Webpack, Vite, Next.js, or Angular builds that process thousands of modules consume significant memory during bundling, tree-shaking, and source map generation.
  • Processing large files: Reading an entire large JSON file or CSV into memory at once (for example with JSON.parse(fs.readFileSync('huge-file.json'))) can exceed the limit quickly.
  • Memory leaks: Event listeners that are never removed, closures that hold references to large objects, global caches that grow without bounds, or accumulating data in arrays during long-running processes.
  • Docker or CI/CD environments: Containers and CI runners often have tight memory constraints. Even if you increase the Node.js heap limit, the container itself may kill the process when it exceeds the container’s memory allocation. This is the same mechanism behind Docker’s OOMKilled (exit code 137).
  • Dependency bloat: Installing heavy packages or importing large libraries you only partially use adds to the baseline memory footprint.

Understanding which of these applies to your situation determines which fix you need.

When to Use Which Fix

The next nine sections cover the fixes in detail. The table below maps your situation to the recommended fix.

Your situationRecommended fixWhy
Quick raise for one commandFix 1: --max-old-space-size flagDirect V8 control
Want global limit for all scriptsFix 2: NODE_OPTIONS env varOne config, many invocations
Memory grows during long runFix 3: find the leak; do NOT just raise the limitRaising hides the bug
Reading large JSON / CSV filesFix 4: streaming parser (stream-json, csv-parser)Avoid ~2x file size in heap
Webpack / Vite / Rollup build OOMFix 5: lighter source-map, thread-loader, chunk splitBuild phase has known memory hot spots
Container OOMKilled (exit 137)Fix 6: raise container memory first, set V8 to 75 percentContainer limit ALWAYS dominates
CI runner OOMFix 7: set NODE_OPTIONS in workflow, skip parallelCI runners are tighter than dev machines
Cannot tell what is leakingFix 8: heap snapshots via --inspect and DevToolsProfile before guessing
Old Node version, want easier defaultsFix 9: upgrade to current LTSV8 has gained pointer compression and better GC

If multiple rows apply, pick the topmost one.

Fix 1: Increase the Memory Limit with --max-old-space-size

The fastest fix is to give Node.js more memory. Pass the --max-old-space-size flag with a value in megabytes:

node --max-old-space-size=4096 your-script.js

This sets the V8 heap limit to 4 GB. Common values:

ValueMemory
20482 GB
40964 GB
81928 GB
1638416 GB

For build tools, you typically need to pass this through the tool’s CLI. For example, with webpack:

node --max-old-space-size=4096 ./node_modules/.bin/webpack --config webpack.prod.js

Or with a Next.js build:

node --max-old-space-size=4096 ./node_modules/.bin/next build

A discipline I have internalized: never set --max-old-space-size higher than 75 percent of physical RAM. On an 8 GB laptop, --max-old-space-size=6144 is the ceiling. Set it higher and the OS starts swapping pages to disk; what was fast crashes becomes slow crashes, and your build time triples. The right answer is always “find what you actually need” not “give it everything.”

Fix 2: Set NODE_OPTIONS Environment Variable

If you don’t want to modify every command, set the memory limit globally through the NODE_OPTIONS environment variable.

On Linux and macOS:

export NODE_OPTIONS="--max-old-space-size=4096"

On Windows (Command Prompt):

set NODE_OPTIONS=--max-old-space-size=4096

On Windows (PowerShell):

$env:NODE_OPTIONS="--max-old-space-size=4096"

To make this permanent in your project, add it to your package.json scripts:

{
  "scripts": {
    "build": "NODE_OPTIONS='--max-old-space-size=4096' webpack --config webpack.prod.js",
    "build:win": "set NODE_OPTIONS=--max-old-space-size=4096 && webpack --config webpack.prod.js"
  }
}

For cross-platform compatibility, use the cross-env package:

npm install --save-dev cross-env
{
  "scripts": {
    "build": "cross-env NODE_OPTIONS='--max-old-space-size=4096' webpack --config webpack.prod.js"
  }
}

Note: If NODE_OPTIONS is already set elsewhere (for example, in your CI pipeline or Docker image), your new value will override it entirely. Concatenate values if you need multiple flags: NODE_OPTIONS="--max-old-space-size=4096 --openssl-legacy-provider".

Fix 3: Fix Memory Leaks in Your Code

Increasing the memory limit is a band-aid. If your application has a memory leak, it will eventually crash regardless of how much memory you allocate. Here are the most common leak patterns in Node.js and how to fix them.

Unbounded Caches or Arrays

// BAD: This array grows forever in a long-running process
const cache = [];

app.get('/data', (req, res) => {
  const result = expensiveQuery(req.params.id);
  cache.push(result); // Never cleaned up
  res.json(result);
});

Fix it by using a bounded cache with an LRU (Least Recently Used) strategy:

import { LRUCache } from 'lru-cache';

const cache = new LRUCache({ max: 500 }); // Maximum 500 entries

app.get('/data', (req, res) => {
  const cached = cache.get(req.params.id);
  if (cached) return res.json(cached);

  const result = expensiveQuery(req.params.id);
  cache.set(req.params.id, result);
  res.json(result);
});

Event Listener Leaks

// BAD: Adding a listener on every request without removing it
app.get('/stream', (req, res) => {
  process.on('SIGTERM', () => {
    res.end('Server shutting down');
  });
});

Node.js warns you about this with MaxListenersExceededWarning. Fix it by removing listeners when they’re no longer needed:

app.get('/stream', (req, res) => {
  const handler = () => {
    res.end('Server shutting down');
  };
  process.on('SIGTERM', handler);

  req.on('close', () => {
    process.removeListener('SIGTERM', handler);
  });
});

Closures Holding Large References

// BAD: The closure keeps `hugeData` in memory as long as `getItem` exists
function createLookup() {
  const hugeData = loadEntireDatabase(); // 500 MB object

  return function getItem(id) {
    return hugeData[id];
  };
}

const lookup = createLookup(); // hugeData is never garbage collected

Fix it by restructuring so you don’t hold the entire dataset in memory, or use a database query instead.

Fix 4: Optimize Large JSON and File Processing

Parsing large JSON files with JSON.parse() requires roughly twice the file size in memory: once for the raw string, once for the parsed object. A 500 MB JSON file needs at least 1 GB of free heap.

Stream Large JSON Files

Instead of loading the entire file at once, use a streaming JSON parser:

import { createReadStream } from 'fs';
import { parser } from 'stream-json';
import { streamArray } from 'stream-json/streamers/StreamArray.js';

const pipeline = createReadStream('huge-file.json')
  .pipe(parser())
  .pipe(streamArray());

pipeline.on('data', ({ value }) => {
  processItem(value); // Process one item at a time
});

pipeline.on('end', () => {
  console.log('Done processing');
});

Stream Large CSV Files

The same principle applies to CSV files. Use a streaming parser like csv-parser:

import { createReadStream } from 'fs';
import csv from 'csv-parser';

createReadStream('large-dataset.csv')
  .pipe(csv())
  .on('data', (row) => {
    processRow(row);
  })
  .on('end', () => {
    console.log('CSV processing complete');
  });

Avoid fs.readFileSync for Large Files

Replace synchronous reads with streams for any file larger than a few dozen megabytes:

// BAD
const data = JSON.parse(fs.readFileSync('large.json', 'utf-8'));

// GOOD
import { createReadStream } from 'fs';
import { pipeline } from 'stream/promises';
// Use a streaming approach as shown above

This is a different issue from typical module resolution errors; the file exists and can be found, but it is simply too large to fit in memory all at once.

How Other Tools Handle This

The “heap out of memory” failure is V8-specific, but every JavaScript runtime has its own memory model and ceiling. Knowing the difference helps you pick the right runtime for the workload rather than fighting the wrong knob.

Node.js V8 defaults. On 64-bit Node, the old generation defaults to roughly 4 GB on modern releases (the often-quoted 1.4 GB number applies to 32-bit builds and very old Node 10 and earlier). --max-old-space-size only controls the old generation; the new (young) generation has its own limit set with --max-semi-space-size, defaulting to a few megabytes. Setting --max-old-space-size to 16384 on a machine with 8 GB of RAM will swap and crash anyway.

Deno uses the same V8 engine as Node and therefore has the same default heap behavior. The flag is identical: deno run --v8-flags=--max-old-space-size=4096 script.ts. Deno does not expose NODE_OPTIONS, so CI configurations from Node do not carry over.

Bun uses JavaScriptCore (the engine from Safari), not V8. JSC has a different garbage collector (a generational collector with concurrent sweeping), different default limits, and does not respect --max-old-space-size at all. Bun reports memory through process.memoryUsage() for compatibility, but the internal heap layout is unrelated to V8 and a memory profile from Chrome DevTools will not match Bun’s actual allocator behavior. Bundling with bun build typically uses less peak memory than webpack for the same input.

Browser V8 (Chrome). Chrome assigns each tab its own V8 isolate with a heap limit around 4 GB on 64-bit systems. WebAssembly memory and ArrayBuffer allocations count separately. If a web page hits this limit, the tab crashes with Aw, Snap!; there is no flag to raise it. Web workers each get their own isolate with the same limit.

Node 22 single-executable applications (SEA). SEA bundles a script into the Node binary itself. The V8 heap behavior is unchanged; but since the binary is monolithic, NODE_OPTIONS set in the environment still works at startup. There is no way to bake --max-old-space-size into the SEA blob; it must be passed at runtime.

Practical takeaway: for build tools, switching from webpack to esbuild, swc, or Rollup typically drops memory usage by 5-10x because they do far less in-memory tree work. The first fix is often “use a less memory-hungry tool” before “give the same tool more memory.”

Fix 5: Optimize Webpack and Vite Builds

Build tools are among the most common triggers for heap out of memory errors. Here are targeted fixes for each.

Webpack

Generate source maps more efficiently. The default source-map devtool is the most memory-intensive option. Switch to a lighter alternative for development:

// webpack.config.js
module.exports = {
  devtool: process.env.NODE_ENV === 'production'
    ? 'source-map'
    : 'eval-cheap-module-source-map',
};

Use thread-loader to offload heavy loaders (like babel-loader or ts-loader) to worker threads:

module.exports = {
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: [
          'thread-loader',
          'ts-loader',
        ],
      },
    ],
  },
};

Split your build into chunks to reduce peak memory usage. If you’re getting OOM during the optimization phase, limit parallel operations:

module.exports = {
  optimization: {
    minimizer: [
      new TerserPlugin({
        parallel: 2, // Limit parallel minification (default uses all CPUs)
      }),
    ],
  },
};

If your webpack build consistently fails, you may also encounter module parse failures alongside the OOM error. Fix the parse errors first, as failed parses can cause webpack to retry and consume more memory.

Vite

Vite typically uses less memory than webpack, but large projects can still hit the limit during production builds (which use Rollup under the hood).

Increase memory for Vite builds:

node --max-old-space-size=4096 ./node_modules/.bin/vite build

If the issue persists, disable source maps for the build or split the build with build.rollupOptions.output.manualChunks:

// vite.config.js
export default defineConfig({
  build: {
    sourcemap: false,
    rollupOptions: {
      output: {
        manualChunks(id) {
          if (id.includes('node_modules')) {
            return 'vendor';
          }
        },
      },
    },
  },
});

Angular, Next.js, and Other Frameworks

Most frameworks provide a way to pass Node.js flags. Check your framework’s documentation, but the pattern is usually:

# Angular
node --max-old-space-size=4096 ./node_modules/.bin/ng build --configuration production

# Next.js
NODE_OPTIONS='--max-old-space-size=4096' next build

# Gatsby
NODE_OPTIONS='--max-old-space-size=4096' gatsby build

Fix 6: Handle Docker Container Memory Limits

Running Node.js inside a Docker container adds another layer. Even if you set --max-old-space-size=8192, the container might only have 512 MB allocated. In that case the kernel’s OOM killer terminates the process before V8’s own limit kicks in.

Set proper memory limits in your docker run command:

docker run --memory=4g --memory-swap=4g your-app

Or in docker-compose.yml:

services:
  app:
    build: .
    deploy:
      resources:
        limits:
          memory: 4G
    environment:
      - NODE_OPTIONS=--max-old-space-size=3072

Note: Set --max-old-space-size to roughly 75% of the container’s memory limit. The remaining 25% is needed for V8 internals, native allocations, buffers, and the operating system overhead inside the container. If you set both values to the same number, the OOM killer will terminate the process before V8 can gracefully handle the out-of-memory condition. This is the same root cause behind container exit code 137.

In your Dockerfile, you can set the environment variable directly:

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
ENV NODE_OPTIONS="--max-old-space-size=3072"
CMD ["node", "server.js"]

Fix 7: Fix CI/CD Pipeline OOM Crashes

CI/CD environments like GitHub Actions, GitLab CI, and Jenkins often have limited memory (GitHub Actions runners get about 7 GB). Large builds frequently OOM in CI even when they work locally.

GitHub Actions

Add NODE_OPTIONS to your workflow file:

jobs:
  build:
    runs-on: ubuntu-latest
    env:
      NODE_OPTIONS: --max-old-space-size=4096
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm run build

GitLab CI

build:
  stage: build
  variables:
    NODE_OPTIONS: "--max-old-space-size=4096"
  script:
    - npm ci
    - npm run build

Reduce Memory in CI Builds

Beyond increasing limits, reduce memory usage in CI:

  1. Use npm ci instead of npm install: it is faster and uses less memory.
  2. Avoid running tests and builds in parallel unless your runner has enough RAM.
  3. Disable source maps in CI if you don’t need them for deployments.
  4. Cache node_modules to avoid reinstalling on every run.

If your CI build fails with an exit code 1 and the logs show the heap error, the fix is the same: increase NODE_OPTIONS or reduce memory consumption.

Fix 8: Profile Memory Usage to Find the Root Cause

When increasing memory limits does not solve the problem (or you want to understand what is consuming memory) use Node.js’s built-in profiling tools.

Use --inspect with Chrome DevTools

Start your application with the --inspect flag:

node --inspect --max-old-space-size=4096 your-script.js

Then open Chrome and navigate to chrome://inspect. Click “Open dedicated DevTools for Node”. Go to the Memory tab and take a Heap Snapshot.

The heap snapshot shows you:

  • Which objects consume the most memory
  • How many instances of each constructor exist
  • The retainer tree showing what is keeping objects from being garbage collected

To find leaks, take two snapshots at different times and use the Comparison view. Objects that grow between snapshots are likely leaking.

Use --inspect-brk for Build Scripts

For build scripts that crash too quickly to attach a debugger, use --inspect-brk to pause execution at the first line:

node --inspect-brk --max-old-space-size=4096 ./node_modules/.bin/webpack

This gives you time to open DevTools and start recording before the memory spike.

Use process.memoryUsage() for Monitoring

Add memory logging to long-running processes to identify when memory starts growing:

setInterval(() => {
  const usage = process.memoryUsage();
  console.log({
    rss: `${Math.round(usage.rss / 1024 / 1024)} MB`,
    heapUsed: `${Math.round(usage.heapUsed / 1024 / 1024)} MB`,
    heapTotal: `${Math.round(usage.heapTotal / 1024 / 1024)} MB`,
    external: `${Math.round(usage.external / 1024 / 1024)} MB`,
  });
}, 10000); // Log every 10 seconds

Key metrics to watch:

  • heapUsed: Actual memory used by JavaScript objects. If this grows continuously, you have a leak.
  • rss (Resident Set Size): Total memory allocated to the process, including native code and buffers.
  • external: Memory used by C++ objects bound to JavaScript objects (like Buffers).

Use the --heap-prof Flag

Node.js 12+ supports heap profiling natively:

node --heap-prof your-script.js

This generates a .heapprofile file that you can load in Chrome DevTools (Memory tab → Load) to see allocation timelines.

A trap I have personally fallen into more than once: trusting heapUsed to tell the whole story. Native memory (Buffers, streams, C++ add-ons) lives under rss and external, not heapUsed. When rss grows but heapUsed stays flat, raising --max-old-space-size will not help; the leak is in native code. Look at external in process.memoryUsage() output, and reach for tools that profile the C++ side (Node’s built-in --heap-prof is V8-only) when JS metrics look clean.

Fix 9: Upgrade Node.js

Newer versions of Node.js include V8 improvements that handle memory more efficiently. Specifically:

  • Node.js 12+: V8 introduced concurrent marking for garbage collection, reducing GC pauses and improving memory management.
  • Node.js 14+: V8’s pointer compression on 64-bit systems can reduce heap size by up to 40% for pointer-heavy workloads.
  • Node.js 20+: Further GC improvements and better defaults for large heaps.

Check your current version:

node --version

Upgrade to the latest LTS version:

# Using nvm
nvm install --lts
nvm use --lts

# Using fnm
fnm install --lts
fnm use lts-latest

After upgrading, your build might work without any memory flag changes. If you’re stuck on an older Node.js version and also hitting file watcher limits, upgrading resolves both issues.

Stranger Causes I Have Tracked Down

If you’ve tried everything above and the error persists:

  • Check for node_modules bloat. Run npx depcheck to find unused dependencies. Remove packages you do not use; each one adds to the build graph and memory usage.
  • Split large monorepo builds. If you’re building a monorepo with many packages, build each package separately instead of all at once. Tools like Turborepo and Nx can orchestrate incremental builds.
  • Move heavy processing out of Node.js. If you’re processing gigabytes of data, consider using a language better suited for it (Python with generators, Go, Rust) or offload to a database query.
  • Check for circular dependencies. Circular imports can cause webpack and other bundlers to re-process modules repeatedly, inflating memory usage. Use madge --circular to detect them.
  • Try node --expose-gc with manual garbage collection. In extreme cases, you can trigger garbage collection manually between heavy operations:
// Start with: node --expose-gc your-script.js
async function processBatches(items) {
  const batchSize = 1000;
  for (let i = 0; i < items.length; i += batchSize) {
    const batch = items.slice(i, i + batchSize);
    await processBatch(batch);

    if (global.gc) {
      global.gc(); // Force garbage collection between batches
    }
  }
}
  • Use worker_threads for parallel processing. Each worker gets its own V8 heap, effectively multiplying your available memory:
import { Worker, isMainThread, workerData } from 'worker_threads';

if (isMainThread) {
  const worker = new Worker(new URL(import.meta.url), {
    workerData: { file: 'chunk-1.json' },
    resourceLimits: {
      maxOldGenerationSizeMb: 2048,
    },
  });

  worker.on('message', (result) => console.log(result));
  worker.on('error', (err) => console.error(err));
} else {
  const data = processFile(workerData.file);
  parentPort.postMessage(data);
}
  • Set resourceLimits on worker threads. The resourceLimits option lets you control memory per worker independently, giving you finer control than --max-old-space-size which applies to the main thread.

  • Check for huge string concatenation. Building a large string with result += chunk in a loop can spike memory far beyond the final string size because V8 may keep intermediate buffers. Use an array with arr.push(chunk) and arr.join('') at the end, or stream the output directly.

  • Disable persistent caching on the build tool. Webpack 5’s cache.type: 'filesystem' and Vite’s optimizeDeps cache can grow without bound across builds. Delete the cache directory (node_modules/.cache, .vite, .next/cache) and rerun. If the OOM disappears, the cache file was the trigger; set a smaller cache.buildDependencies scope or switch back to cache: false for memory-constrained CI runs.

  • Mind the OOM killer score on Linux. Even with enough swap configured, Linux’s OOM killer scores Node aggressively because it allocates large anonymous mappings. cat /proc/<pid>/oom_score shows the current score. Setting oom_score_adj to a negative value for critical Node processes (or adjusting cgroup memory limits) prevents the kernel from terminating the wrong process under pressure.

What Other Tutorials Get Wrong About This Error

Most Node.js memory tutorials list the same fixes but frame them in ways that produce subtle bugs.

They tell readers to bump --max-old-space-size first. That works as a triage step but hides leaks. The right ordering is: confirm the workload genuinely needs more memory (heap profile), THEN raise the limit. Articles that lead with the flag train readers to skip diagnosis.

They omit the container-limit dominance. Setting --max-old-space-size=8192 inside a 2 GB container does nothing useful: the kernel OOM-kills the process before V8’s own limit fires. Tutorials that focus only on V8 leave container users wondering why nothing changed.

They confuse heapUsed with total memory. Native memory (Buffers, streams, C++ add-ons) lives outside heapUsed. Tutorials that present heapUsed as the only diagnostic miss leaks in native code, which --max-old-space-size cannot help with.

They miss that JSON.parse peaks at roughly 2x file size. A 500 MB JSON file needs about 1 GB of heap just to parse. Articles that recommend JSON.parse(fs.readFileSync(...)) for “small files” rarely define “small,” and the line where this becomes a problem is closer to 100 MB than 1 GB.

They omit cross-env for cross-platform NODE_OPTIONS. NODE_OPTIONS='...' command syntax does not work on Windows cmd.exe. Articles that show only the Unix syntax break for Windows readers. cross-env is the universal shim.

They omit alternative build tools. Webpack with full source maps can consume 5 to 10 times the memory of esbuild or swc for the same input. Tutorials that focus only on raising Webpack’s limit miss that switching tools is often easier than tuning V8.

Frequently Asked Questions

How much memory does Node.js have by default?

On modern 64-bit Node (16+), the default V8 old-generation limit is around 4 GB. Older quoted numbers (1.4 GB, 1.5 GB) apply to 32-bit Node and very old Node 10 and earlier. The exact number depends on the V8 version bundled with your Node release; check with node --v8-options | grep max-old-space-size.

Should I always set --max-old-space-size to my machine’s RAM?

No. Set it to roughly 75 percent of available RAM. The remaining 25 percent is needed for the OS, native allocations, and other processes. Setting it higher causes paging to disk, which makes everything slower without preventing the crash.

Why does the same script work locally but crash in CI?

CI runners typically have less RAM (GitHub Actions free tier has 7 GB) and run more concurrent processes than a developer laptop. The same workload that fits in 16 GB locally may not fit in 4 GB in CI. Set NODE_OPTIONS in the workflow and consider reducing build parallelism.

What is the difference between heapUsed, rss, and external in process.memoryUsage?

heapUsed is V8’s tracked JS object memory. rss (resident set size) is the OS-visible total memory of the process, including native code, buffers, and stack. external is C++-bound memory that V8 manages references for. A leak in heapUsed is fixed by JS-side changes; a leak in external or rss requires C++ profiling.

Does upgrading Node help with memory?

Often yes. Node 14+ has V8 pointer compression (up to 40 percent heap reduction for pointer-heavy workloads). Node 20 LTS has further GC improvements. If you are on Node 12 or 14 and hitting memory walls, upgrading to current LTS is the first thing to try.

When should I use worker_threads instead of --max-old-space-size?

When a single task is CPU-bound or memory-bound enough to need isolation. Each worker thread gets its own V8 heap, so spawning workers effectively multiplies your available memory. For build tools, this is what thread-loader does. For data processing scripts, manual worker_threads with resourceLimits.maxOldGenerationSizeMb gives precise per-task control.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles