JavaScript Heap Out of Memory Error: How to Fix It in Node.js

What is the JavaScript heap out of memory error and what should you do first?
JavaScript heap out of memory is a fatal Node.js error. It means the V8 engine filled its heap and the garbage collector could not free enough space. Your first move is to raise the memory limit temporarily, then find the job that actually consumes the memory.
Do not panic. In most cases a one line setting gets you past the crash for now. However, the permanent fix lives somewhere else. We start with the quick steps, then move to the causes.
- Note which command throws the error (a build, a server, or a script).
- Raise the limit for one run:
node --max-old-space-size=4096 app.js(the value is only an example). - Run the same command again and see whether the error disappears.
- If it returns later, suspect a memory leak or oversized data.
- Follow the diagnostic order in this guide and apply a lasting fix.
This order is what we see work in the field. So we go from the most likely cause to the least likely one.
Why does Node.js throw this error in the first place?
Node.js runs JavaScript on the V8 engine. V8 keeps objects in a memory region called the heap. That region has an upper limit, and the limit can be lower than the total memory of your machine.
When the heap fills up, V8 first tries garbage collection. In other words, it tries to clean up objects that nothing uses anymore. If the cleanup frees too little space, the process stops and you see a message like this: FATAL ERROR: ... JavaScript heap out of memory.
In short, the error does not always mean the machine ran out of memory. Often you simply hit the V8 limit. This difference matters when you pick a fix, because raising the limit helps but does not remove the cause.
The exact wording can vary with the Node.js version. So if your screen shows a slightly different text, treat it as a similar warning and follow the same diagnostic path.
How do you apply the quick fix with the --max-old-space-size flag?
The flag sets the heap limit that V8 may use for long lived objects. The Node.js documentation defines the value in MiB. You write the flag before the script name when you run the command.
node --max-old-space-size=4096 build.js
The value 4096 is only an example. The right number depends on your machine memory and on the other jobs that run at the same time. For instance, on a machine with 8 GB of memory, a value of 4096 leaves room for the operating system and other processes.
After you set the flag, one of two things happens. In the first case the job finishes, and the old limit was probably just too tight. In the second case the error comes later, which usually points to a leak or uncontrolled growth.
So raise the limit once and observe the result. Do not keep raising it blindly. Otherwise you hide the real problem for months.
How do you set the memory limit with NODE_OPTIONS?
Sometimes you cannot edit the command itself, for example inside a package script or a CI step. In that case you use the NODE_OPTIONS environment variable. It carries accepted flags into the Node.js process from the outside.
export NODE_OPTIONS="--max-old-space-size=4096"
npm run build
According to the Node.js documentation, command line options take precedence over options passed through NODE_OPTIONS. So if you set the same flag twice with different values, the command line wins. This detail explains many reports that say the setting has no effect.
Also confirm that the variable reaches the process that fails. A build tool may start child processes, and the flag can reach them through a different path. Therefore look for the setting in the failing process, not only where you added it.
For long term use, write the setting into the project script or the deployment configuration. Then everyone on the team runs with the same limit.
How do you choose a limit without exceeding system memory?
Start with the real memory of the machine. If you set the limit above physical memory, the operating system swaps to disk. The application slows down, and the system may even kill the process by force.
A safe approach looks like this. Think about the other jobs on the machine, leave them a share, and give part of the remainder to Node.js. Also remember that the V8 heap is only one part of the total memory a process uses. Buffers and native add ons consume memory outside the heap.
We do not give a numeric rule, because the right value depends on the workload. Instead we suggest this order:
- Run the failing job in a place where you can watch memory use.
- Raise the limit step by step and record each result.
- When the job finishes, add a small margin to the peak memory it used.
- Keep the limit below the memory of the machine.
The default limit can change between versions, so check the current value in the Node.js documentation.
Why is the flag only a band aid and not a permanent fix?
Raising the limit is like buying a bigger bucket. If the tap keeps running, the bucket still fills. In an application with a memory leak, a higher limit only postpones the crash.
On the other hand, if the problem comes from one genuinely large data job, a higher limit can be a reasonable bridge. Even then it carries two risks. First, the garbage collector scans a bigger heap, so pauses can grow longer. Second, the real problem grows unnoticed because the crash no longer warns you.
Example scenario: a team fixes a build failure by raising the limit. Months later the project grows and the error returns. They raise the limit again. Eventually the CI machine is too small, because nobody ever examined the root cause.
So treat the flag as a tool that buys time. Then move on to the diagnostic order below and apply a lasting fix.
What are the most common causes of JavaScript heap out of memory?
We list the causes in the order we see them in the field, starting with the most likely one. Check the most common cause first, because it saves time.
- A large build: many modules, source maps, and plugins consume memory in the same process.
- Loading big data at once: reading a whole file or parsing a huge response in one piece.
- A memory leak: memory that grows slowly in a long running server.
- An unbounded cache or queue: a list or object that is never emptied.
- A container or CI limit: an environment with less memory than the app needs.
Each cause has its own symptom. A build failure usually repeats at the same step. A leak looks different, because the server runs healthy for hours or days and then crashes. This difference speeds up the diagnosis.
The next sections handle each cause one by one. Also, the comparison table later in this guide connects each symptom to its cause.
Why does a large build run out of memory?
In modern front end projects the build tool loads the whole dependency tree into memory. Then it transforms, bundles, and minifies the code. As the project grows, the peak memory need of this process grows too.
You usually see the error at npm run build. For example, a build that runs fine on your laptop can crash on a CI machine with less memory. The code did not change, only the environment did.
For build related failures, ask these questions in order:
- Does the error always appear at the same step, such as minification?
- Is source map generation turned on?
- How many files are processed in parallel?
- Did a large dependency change recently?
If you got an install error after adding a dependency, our npm ERESOLVE guide can help. Forcing past a dependency conflict sometimes brings in packages that bloat the build.
Why does loading big data into memory trigger this error?
Reading a file or a database result in one call moves the whole content into the heap. For example, if you turn a CSV file of several hundred megabytes into one string, both the raw text and the parsed objects stay in memory. So the memory you need becomes several times the file size.
Similarly, parsing a large JSON response in one piece needs more memory than the response itself. Parsing creates intermediate objects as well. That is why code that worked yesterday can suddenly fail when the data grows.
The typical symptom is this: everything works with small sample files and crashes with real data. In other words, the code is not wrong, it just does not scale.
The direction of the fix is clear. Process the data in pieces, not as a whole. The next section explains how.
How does stream processing protect memory?
A stream reads and processes data in small chunks. Think of water in a pipe instead of water in a bucket. Only a small amount sits in memory at any time. The Node.js documentation presents this model as the stream API.
Conceptually there are three steps. First you open the source as a stream. Then you process each chunk. Finally you write the result to the destination. Because chunks pass through one by one, memory use does not grow with the file size.
const { pipeline } = require('node:stream/promises');
await pipeline(input, transform, output);
Line based files follow the same idea, because you handle each line separately. With database queries, fetching results page by page is much safer than loading the whole table.
Watch one point with streams: a slow destination. If the destination cannot keep up, data can pile up in memory. So build the chain with pipeline helpers instead of raw event listeners. For details, read the Node.js stream documentation.
How do you tell whether you have a memory leak?
A memory leak happens when objects you no longer need are still referenced from somewhere. The garbage collector removes only unreachable objects. So an object forgotten in a list stays forever.
The symptom is usually this. The app starts, memory climbs slowly, and it never goes down. Even when traffic drops, memory stays the same. Eventually the process crashes, restarts, and the cycle repeats.
Common leak patterns include these:
- Global caches and object maps that are never cleaned.
- Event listeners that are never removed.
- Timers and connections that are never closed.
- Arrays that grow with every request and are never emptied.
To prove a leak, track memory use over time. If you see a steadily rising staircase instead of a flat line, the suspicion gets stronger. Reading server logs also helps, and you can use our free log file analyzer for that.
How do you take and read a heap snapshot?
A heap snapshot is a photo of the heap at one moment. It shows which object types take how much space and who holds them. It is the strongest tool for leak hunting.
According to the Node.js documentation, the --heapsnapshot-signal flag makes the process write a heap dump when it receives the signal you choose. The --heapsnapshot-near-heap-limit option writes a dump when the heap approaches its limit. However, the documentation also notes that taking a snapshot needs time and memory.
node --heapsnapshot-signal=SIGUSR2 server.js
kill -USR2 PROCESS_ID
The best method is to take two snapshots: one right after startup and one after memory has grown. Then compare them. An object type that clearly increases in the second snapshot points to the leak.
Be careful in production. A snapshot file can contain sensitive data, and the process slows down while it is written. So store the file safely and delete it when you finish.
How do you inspect memory with --inspect and Chrome DevTools?
Node.js opens a debugging port with the --inspect flag. Chrome DevTools connects to it. In the memory panel you can take heap snapshots, sort object types, and follow reference chains.
node --inspect server.js
After you connect, you take snapshots in the Memory panel. The comparison view lists the difference between two snapshots. So you quickly see which objects pile up. For details, read the Chrome DevTools memory documentation.
Security note: the Node.js documentation warns that binding the inspector to a public IP and port is insecure. Therefore use the debugging port only on your local machine or through a secure tunnel.
If the connection fails, the cause is often the port. In that case our localhost refused to connect guide walks you through the checks.
How do you tune parallelism and source maps in build tools?
Two settings often make the difference in build related memory problems. They are the number of parallel jobs and the source map output. Option names differ between tools, so verify the exact name in your tool documentation.
The logic of parallelism is simple. Every parallel job asks for its own share of memory. So if you reduce the number of concurrent jobs, peak memory drops, but the build takes longer. On a memory limited CI machine this trade is often worth it.
Source maps make debugging easier, but producing them costs memory. In production builds, choosing a lighter map type or turning maps off can lower memory use noticeably. Still, the decision depends on your error tracking needs.
Also consider these steps:
- Remove unused dependencies.
- Import only the part of a large library that you need.
- Enable the build cache and keep it clean.
Page speed is related as well. For that topic, see our article on how JavaScript affects site speed.
Why is the memory limit in CI and containers a separate problem?
A process in a container may not see all the memory of the host. If the container has a memory limit, the operating system kills any process that exceeds it. In that case the process can disappear without a V8 error message at all.
You have to separate two limits. The first is the V8 heap limit, which you set with --max-old-space-size. The second is the memory limit of the container or CI runner. If the V8 limit is larger than the container limit, the container kills the process before V8 can report anything.
So keep the V8 limit below the container limit. Then the failure arrives as a readable message instead of a silent exit. To see how container resource limits are defined, read the Docker resource constraints documentation.
If you want to start from the basics, our guide to what Docker is is a good entry point. Remember that the same code can run locally and crash in CI, because environments do not offer equal memory.
Should you use a PM2 memory threshold restart?
PM2 is a process manager for Node.js applications. It has an option that restarts a process automatically when memory passes a threshold. You define the threshold in your configuration file, and you should verify the exact option name in the PM2 documentation.
This feature reduces user facing crashes until you find the leak. In other words, the server is refreshed in a controlled way instead of dying at night and staying down until morning. In production it works as a temporary safety belt.
On the other hand, it does not solve the problem. A restart does not remove the leak, it only softens the result. Open connections can also drop during the restart. So think of the threshold as a bridge, not as a cure.
Our advice is this. Turn on the PM2 threshold, hunt the leak with heap snapshots at the same time, and keep the threshold as a safety net afterward. To learn PM2 from scratch, read our guide on what PM2 is.
Does JavaScript heap out of memory only happen on servers?
No. The error can appear anywhere Node.js runs. Package installs, test runners, type checkers, build tools, and one off data scripts are the usual places. So the problem is tied to the memory behavior of the Node.js process, not to the word server.
For example, a developer can see the same error on a laptop while running the test command. In that case no long running server exists, but a large test suite eats the memory. Therefore the diagnostic question is always the same: which command, at which stage, with how much data?
This distinction matters because the fixes differ. In a one off script, raising the limit can be reasonable. In a continuously running server, the same move hides a leak. So first write down which kind of process fails, then go to the matching section.
What diagnostic order should you follow for this error?
The order we follow goes from the cheapest check to the most expensive one. This way you do not waste hours. Change only one thing at a time, because if you change three settings together, you cannot tell which one helped.
- Record which command and which stage throw the error.
- Compare environments: is memory different on your machine, in CI, and in the container?
- Raise the limit once and see whether the error is delayed.
- If it is delayed, measure memory growth over time.
- When growth is steady, take two heap snapshots and compare them.
- If growth follows the data size, switch to streams or paging.
- Apply the lasting fix and add a memory alert.
The logic is simple. Comparing environments is the cheapest check and often the most common cause. Taking a snapshot is the most expensive step, so it sits near the end.
Also save a short note after every attempt. If you write down how much the error was delayed at each limit, you can see whether the problem grows with data or with time.
How do garbage collection and heap size affect application speed?
While the garbage collector runs, the application can pause for a short time. As the heap grows, more objects need scanning and pauses can get longer. So keeping the limit very high can hurt response times even if you never see the error.
You recognize the symptom like this. The app does not crash, but it freezes for a few seconds from time to time. On a memory graph you see a sawtooth pattern. In other words, the collector runs often and for a long time.
In that case a bigger heap rarely helps. Instead you need to create fewer objects. For example, reuse large objects that you rebuild on every request, and keep temporary arrays small.
In short, memory is not only a crash problem, it is a performance problem too. The lasting fix is a design that uses less memory.
Example scenario: how do you fix this error in a reporting script?
Imagine a script that reads all order records every night and writes one report file. It runs fine for the first months. When the record count grows, one night it crashes with a JavaScript heap out of memory error.
The team first raises the limit, and the script runs again. A few months later the error returns. This time they diagnose it and see that the script collects every record in one array. Put simply, the memory need grows directly with the record count.
The lasting fix has these steps:
- Fetch records page by page.
- Process each page and write it to the output right away.
- Release the processed page from memory.
- Measure the peak memory of the script once and write it down.
As a result, memory use no longer depends on the record count. This scenario is fictional, but it reflects a pattern we often see in the field.
When should you change the architecture instead of the flag?
Some jobs do not belong in memory. Converting very large files, merging huge data sets, and heavy image processing are examples. Running them inside the main application process puts all users at risk.
So move heavy jobs to a separate process or to a queue based worker. Then a memory problem hits only that worker, and the main application keeps running. You can also give the worker its own memory limit.
On the other hand, not every problem needs an architecture change. If the job is small and one off, raising the limit is enough. To decide, ask this: will the job keep growing? If the answer is yes, fixing the design now is cheaper than fixing it later.
Which method should you choose and when?
The table below compares the methods by when to use them and by risk. It summarizes the diagnostic order in this guide.
| Method | When to use | Risk |
|---|---|---|
| Raise the memory limit | You need a quick temporary fix | Hides leaks and lengthens pauses |
| Stream processing | You handle a large file or data set | Code must be rewritten |
| Heap snapshot | Memory grows over time | Slows production and may contain sensitive data |
| Build settings | The error appears only during the build | Longer build, weaker error tracking |
| Raise the container limit | Silent exits happen | Higher cost |
| PM2 memory threshold | You need a bridge until the leak is found | Connections may drop |
Keep one thing in mind while you read the table: no method is enough alone. In most real cases you first raise the limit, then find the cause, and finally apply the lasting method.
Which checklist gives you a lasting prevention plan?
After you fix the problem, follow this list to stop it from coming back. It works as a short maintenance routine for software teams.
- Monitor memory use regularly and define an alert threshold.
- Always process big data with streams or paging.
- Add a size limit and an expiry to every cache.
- Remove event listeners and timers when the work ends.
- Document the memory limits of CI and containers.
- Update dependencies regularly and delete unused ones.
- Use a safety threshold with a manager such as PM2 in production.
- Run a memory test once before every major release.
In short, the goal is to make memory visible instead of waiting for the error. This way you catch the problem before it reaches users.
It also helps to tie this list to a maintenance calendar. For example, make the memory check a shared step before each release. Then new developers pick up the same habit. If you run a Node.js application on a server, our Node.js deployment guide covers the setup steps.
How can you get help from our team with this problem?
Memory problems sometimes need more than one setting. Leak hunting, build pipeline tuning, and redesigning a large data flow take time and experience. At this point Talha Aslan and our team support you through our custom software development service.
Describe your need briefly, and we define the scope together. For details, visit our custom software development page and request a quote there.
The steps in this guide are enough for you to make progress on your own in most cases. Also, the npm and localhost guides from the same series cover other common problems in the same development environment.
Note: this content is general information. Verify command and option names in the official documentation of your Node.js version, because options can change over time. Also use the Node.js command line documentation as your main source.



