Memory in Node.js
Learn how Node heap limits, memory counters, Buffers, heap snapshots, and out-of-memory reports help you keep servers healthy.
- 01Set a safe heap limitExplain the V8 heap limit, and keep it below a container's memory limit.
- 02Read the memory countersTell apart rss, the heap counters, external memory, and ArrayBuffer memory in a Node process.
- 03Follow up a crashSave a heap snapshot, and turn a 'JavaScript heap out of memory' crash into a clear next step.
Memory in a Node process
Node runs JavaScript outside the browser, often on a server. A running Node program is called a process. The process needs memory for many things, not only your JavaScript objects.
Inside every Node process is V8. V8 is the engine that runs your JavaScript. V8 keeps your objects, arrays, and strings in an area of memory called the heap.
The process also holds memory outside the heap:
- Node's own built-in code.
- Raw bytes from files, the network, and images.
- Libraries written in other languages, such as C++.
So when memory grows, first ask which kind of memory is growing.
Node process memory is all the memory that one running Node program holds. The V8 heap is one important part. Buffers (blocks of raw bytes) and Node's built-in code can use memory outside the heap.
This lesson continues from Memory in large apps. That lesson asked whether an app keeps data for too long. A server has one more question: can the process stay under its memory limit while it handles real requests?
In this lesson you will:
- read the memory numbers that Node reports,
- choose a limit for the heap,
- save a picture of the heap to a file,
- read the message Node prints when the heap runs out.
These tools don't fix memory problems by themselves. They point you to the part of the code you need to look at.
Bytes before memory counters
A byte is the smallest unit of memory you will meet here. One letter like a takes one byte. Node reports memory as a count of bytes, so it helps to see one familiar amount first.
const bytes = new ArrayBuffer(1024 * 1024);console.log(bytes.byteLength);An ArrayBuffer is a block of raw bytes. It works in the browser too, so you can run this code here.
- Line 1 makes an
ArrayBufferwith 1024 × 1024 bytes. - Line 2 prints its length in bytes:
1048576.
1048576 bytes is one mebibyte (MiB). Many tools just call it one megabyte (MB). This lesson says MB too.
Server tools often show big numbers such as 10485760. To turn bytes into MB, divide by 1024 two times. 10485760 bytes is 10 MB.
Memory numbers change all the time while a process works. One number from one moment tells you little. Look for a change that happens again and again. For example: memory grows by about the same amount after every file upload.
Heap limits and containers
V8 does not let the heap grow forever. It has a heap limit: the most memory the heap may use. You can read the current limit with v8.getHeapStatistics().heap_size_limit.
const v8 = require("node:v8");console.log(v8.getHeapStatistics().heap_size_limit);- Line 1 loads
node:v8, a module built into Node that reports on V8. - Line 2 prints the heap limit in bytes.
This code works only in Node, so there is no Run button here. The lesson test runs it twice: once with a 32 MB limit and once with a 64 MB limit. It checks that the 64 MB run prints a bigger number.
You set the limit with a flag, an option you type after node when you start a program:
node --max-old-space-size=64 app.jsThe number is in MB. "Old space" is the biggest part of the heap: the part where long-lived objects stay. So this flag caps the heap. It does not cap the whole process. The process can still use more memory for:
- Buffers and other raw bytes,
- Node's built-in code,
- stacks: the memory that tracks which functions are running right now.
Servers often run inside a container. A container is a sealed box for one app, and it has its own memory limit for the whole process. The step-through below works out how much room is left when the container allows 128 MB and the heap gets 64 MB.
Step through a small sum. It shows why the heap limit must stay below the container's limit.
script
const containerMB = 128;const headroomMB = containerMB - limitMB;console.log(headroomMB);Your lunch box has a fixed size. When it is full, you put extra food in a separate bag. The bag is not inside the lunch box, but you still carry it.
- In real life: A lunch box holds only so much
- In JavaScript: The V8 heap has a limit
- In real life: Extra food goes in a separate bag
- In JavaScript: Buffers use memory outside the heap
- In real life: You still carry both
- In JavaScript: The process holds the heap and the outside memory
- In real life: The bag does not make the lunch box bigger
- In JavaScript: Buffer bytes don't count as heap memory
In a container, set the heap limit below the container's limit. The gap between them is called headroom: room for everything that is not in the heap. Without headroom, the operating system may stop the whole process with no warning. Then you get no helpful message about the heap.
Reading process.memoryUsage()
process.memoryUsage() asks Node how much memory the process uses right now. It returns an object with five numbers. Each number is a counter for one kind of memory, in bytes. No single counter tells the whole story, so read them together.
const usage = process.memoryUsage();console.log(Object.keys(usage).join(","));- Line 1 asks Node for the counters right now.
- Line 2 prints the counter names, joined with commas. In Node it prints
rss,heapTotal,heapUsed,external,arrayBuffers.
The table below explains each counter. Read each row as: "this counter measures this, and here is what to remember about it."
| Counter | What it measures | What to remember |
|---|---|---|
rss | All the memory the process holds in RAM, the computer's working memory. | It includes the heap, Node's own code, stacks, and Buffer bytes. |
heapTotal | How much memory the V8 heap has set aside right now. | This is room the heap has taken, not what your objects use. |
heapUsed | How much of the heap your JavaScript objects use right now. | It shows objects that keep piling up, but it leaves out Buffer bytes. |
external | Memory outside the heap that V8 keeps track of. | Buffers usually make this grow. It already includes arrayBuffers. |
arrayBuffers | The part of external used by ArrayBuffers and Node Buffers. | It is part of external, so don't add the two together. |
Two counters overlap. arrayBuffers is already counted inside external. So don't add those two together. You would count the same bytes twice.
Read the counters at clear moments: before a batch of work, after it, and again after the work should have been let go. Don't log them on every request. That makes a lot of noise, and the log strings use memory too. For a server that runs for days, save the same five counters every few minutes and watch the trend.
Buffers and external memory
A Buffer is Node's type for a block of raw bytes. You meet Buffers when you read files, receive data from the network, or work with images.
The bytes of a Buffer are not stored in the V8 heap. They live outside it. So a big Buffer makes external grow a lot, while heapUsed barely moves.
const before = process.memoryUsage();const data = Buffer.alloc(10 * 1024 * 1024);const after = process.memoryUsage();console.log("buffer bytes", data.length);console.log("external grew", after.external > before.external);console.log("arrayBuffers grew", after.arrayBuffers > before.arrayBuffers);- Line 1 saves the counters before.
- Line 2 makes a Buffer of 10 MB.
Buffer.alloc(n)makes a Buffer ofnbytes, all set to zero. - Line 3 saves the counters after.
- Line 4 prints the Buffer's size:
10485760bytes. - Lines 5 and 6 print whether
externalandarrayBuffersgrew. Both printtrue.
The exact numbers are different on every computer. So the lesson test checks only facts that are always true: external grows by at least 10 MB, and heapUsed grows by much less.
The step-through below uses a small model with round numbers, so you can follow the change.
Step through a small model with round numbers. It shows which counter a Buffer's bytes go to. It doesn't measure real memory.
script
const bufferMB = 10;const after = { heapUsedMB: 3, externalMB: 11 };console.log(after.externalMB - before.externalMB);external grows by 10 MB and heapUsed stays the same.const bytes = sizeMB * 1024 * 1024;const data = Buffer.alloc(bytes);console.log(data.length);3 MBThe heap. It stays the same, because the bytes are not here.
11 MBMemory outside the heap. The Buffer's bytes are counted here.
10 MBPart of external. The Buffer's bytes show up here too.
A 10 MB Buffer makes external grow to 11 MB and arrayBuffers grow to 10 MB. heapUsed stays at 3 MB, because the Buffer's bytes are outside the heap.
A Buffer is outside the heap, but the process still holds its bytes. So keep binary work small and limited:
- Limit how many uploads run at the same time.
- Don't keep every file in one big array.
- When a request ends, drop your links to its Buffers.
When your server handles a lot of files, watch rss and external together.
Heap snapshots in Node.js
A heap snapshot is a file that lists every object in the V8 heap at one moment, and what keeps each one alive. It is most useful in two cases:
- You compare a snapshot from before some work with one from after.
- You look for what still holds a list that should have been thrown away.
There are three ways to make one in Node.
const v8 = require("node:v8");const filename = v8.writeHeapSnapshot();console.log(filename);- Line 1 loads the built-in
node:v8module. - Line 2 writes the snapshot to a file and keeps the file's name.
- Line 3 prints that file name.
Making a snapshot pauses your program and needs extra memory. So only do it on purpose, on a machine where a pause is safe. To read the file, open it in the Memory panel of Chrome DevTools.
The second way uses a signal. A signal is a short message that you send to a running process from the terminal.
node --heapsnapshot-signal=SIGUSR2 app.js
kill -USR2 <pid>- Line 1 starts Node and tells it: "when you get the signal
SIGUSR2, save a snapshot." - Line 2 sends that signal. Put the process's ID number in place of
<pid>.
This helps when you can't easily add code at the moment the problem happens. On a real server, follow your team's rules for sending signals to live processes.
The third way saves a snapshot just before the heap runs out:
node --max-old-space-size=64 --heapsnapshot-near-heap-limit=1 app.jsThis starts Node with a 64 MB heap limit. When the heap gets close to that limit, Node saves one snapshot. The snapshot is a clue for later. It does not stop the crash. To learn how to read it, go on to Heap snapshots in depth and look for what keeps the objects alive.
Diagnosing out-of-memory crashes
An out-of-memory crash (OOM for short) happens when the heap is at its limit and V8 has no room for the next new value. Node stops and prints a report. The report contains a line like FATAL ERROR: ... JavaScript heap out of memory.
Look for that phrase first. It tells you the heap was full. A container can also stop a process for other reasons, and then this phrase is missing.
const orders = [];while (true) { orders.push("tea ".repeat(10000));}- Line 1 makes the
ordersarray. The array stays in use, so nothing in it can be thrown away. - Line 2 starts a loop that never ends.
- Line 3 adds a long string to the array each time: the word
"tea "repeated 10000 times.
Run code like this only in a separate test process with a tiny limit. The lesson test runs it with --max-old-space-size=32. It checks two things: the process ends with an error, and its output contains heap out of memory.
orders array keeps every string, so the heap fills up. When the next string can't fit, Node stops with "heap out of memory".You keep putting food into a lunch box that is already full. The lid will not close. So you stop, instead of pretending the new food fits.
- In real life: You keep adding food
- In JavaScript: The array keeps every new string
- In real life: The lunch box is already full
- In JavaScript: The heap is at its limit
- In real life: The lid will not close
- In JavaScript: The next string has no room
- In real life: You stop packing
- In JavaScript: Node stops with an error
The table below lists common reasons a heap fills up. Read each row as: "this is the cause, this is what happens, and this is what to try first."
| What causes it | What happens | What to try first |
|---|---|---|
| An array or cache with no size limit | It keeps taking new entries and never removes old ones. | Give it a largest size, or remove entries after a set time. |
A huge JSON.parse | The long text and all the objects parsed from it are in memory at the same time. | Send the data in smaller pages, limit the request size, or stream it. |
| Reading a whole file at once | readFile loads every byte before your code starts on it. | Stream the file, so you handle one small piece at a time. |
| Many large Buffers | Their bytes make external and rss grow. | Limit how many jobs run at once, and drop each Buffer when its job ends. |
When you see an OOM report, follow these steps:
- Check that the report says
JavaScript heap out of memory. - Compare the heap counters and the
externalcounter. - Make the crash happen again, with a small, safe limit.
- Save a heap snapshot if you can.
- Find the list or cache that keeps growing, and what holds it.
A bigger heap limit only makes the crash come later. If an array or cache grows forever, it will fill the bigger heap too.
A small server memory routine
Start with a memory budget:
- Find out the container's memory limit.
- Choose a smaller heap limit, and leave headroom.
- Pick one piece of work to measure, such as one batch of uploads or one report. Read the counters before it and after it.
Then put a limit on everything that can grow:
- A cache needs a largest size, or a time after which old entries are removed.
- A big file should be streamed: read and handled one small piece at a time, instead of all at once.
- A server that accepts JSON needs a size limit for each request. Without one, a single huge request holds a huge string and all the objects parsed from it, both at the same time.
- An ordinary
cartobject with product names. - A growing array of parsed order objects.
- A 10 MB value from
Buffer.alloc(...). - Memory held by a database library written in C++.
- Stacks and Node's own built-in code.
- The
rssnumber fromprocess.memoryUsage().
Put each card in the group you would check first. Each answer names the counter or tool that helps.
Watch for a trend, and connect it to one kind of work. Each trend points to a different place to look:
heapUsedgrows after every finished job: your code is probably keeping JavaScript objects it no longer needs.externalgrows while images are processed: too many Buffers are in use at the same time.rssgrows, but the other two don't: look at libraries written in other languages and at Node itself.
Common mistakes
- "heapUsed is all the memory the process uses." It counts only the V8 heap.
rsscounts the whole process. - "A Buffer can't fill up memory." A Buffer's bytes are outside the heap, but the process still holds them.
- "The heap limit should equal the container limit." The process needs headroom for memory outside the heap.
- "A bigger heap fixes the bug." It only makes the crash come later, while the array or cache keeps growing.
- "A heap snapshot shows every byte the process uses." It focuses on the objects in the V8 heap. Node's own code, the stacks, and much of the rest of
rssare not in it.
Here is the short version. Heap tools explain which JavaScript objects are kept alive. The process counters show the memory of the whole process. You often need both. A snapshot finds the array that keeps growing. rss and external show how much a server that handles many files really uses.
Practice exercises
new TextEncoder().encode(text) turns text into bytes. Read the program and type what it prints. This one runs in the browser.
const bytes = new TextEncoder().encode("tea");
console.log(bytes.length);It prints 3. Plain English letters like the ones in tea take one byte each.
What does this headroom program print?
const containerMB = 128;
const heapMB = 64;
console.log(containerMB - heapMB);It prints 64. That is the room left for memory outside the heap.
A server makes many large Buffers. Which counter from process.memoryUsage() should you check first?
Watch external and arrayBuffers. A Buffer can be large even when heapUsed changes only a little.
You saved a heap snapshot file. Where do you open it?
Open the file in the Memory panel of Chrome DevTools. Then look at the summary and at what keeps each object alive.
Your server prints JavaScript heap out of memory. Name one thing from this lesson that you would check first.
Start with an array or cache that has no size limit, a huge JSON.parse, or reading a whole file at once. Measure first, before you change the heap limit.
An upload server keeps each file in a Buffer while it is being uploaded. What should the server limit before more people start using it?
The editor shows the same idea for a cache: remove old entries so the size stays small. It prints 1.
const cache = new Map();
cache.set("first", "tea");
cache.set("second", "milk");
cache.delete("first");
console.log(cache.size);Limit how many uploads run at the same time, and limit the size of each file. Then the Buffers have a known largest total, so rss and external stay predictable.
Check your understanding
For each question, first ask which kind of memory it is about: the heap, memory outside the heap, or the whole process.
Question 1 of 7What does
--max-old-space-size=64limit?Choose an answer to see the explanation.
Question 2 of 7What does this code print? It runs in the browser too.
Read the code, then predictconst bytes = new ArrayBuffer(1024 * 1024); console.log(bytes.byteLength);Choose an answer to see the explanation.
Question 3 of 7A server makes a 10 MB Buffer. Which counter shows it best?
Choose an answer to see the explanation.
Question 4 of 7In a container, why should the heap limit be lower than the container's memory limit?
Choose an answer to see the explanation.
Question 5 of 7Which command makes Node save a heap snapshot when it gets the signal
SIGUSR2?Choose an answer to see the explanation.
Question 6 of 7Which words in a crash report tell you the V8 heap was full?
Choose an answer to see the explanation.
Question 7 of 7What does this cache program print?
Read the code, then predictconst cache = new Map(); cache.set("first", "tea"); cache.set("second", "milk"); cache.delete("first"); console.log(cache.size);Choose an answer to see the explanation.
Key takeaways
- The heap limit caps the V8 heap only, not the whole process.
- In a container, set the heap limit lower than the container limit. The gap is headroom for memory outside the heap.
- Read
rss,heapTotal,heapUsed,external, andarrayBufferstogether, before and after a piece of work. - Buffers make
externalandarrayBuffersgrow much more thanheapUsed. - A heap snapshot shows which objects are kept alive. The counters show the memory of the whole process.
- After an OOM, find what keeps growing before you raise a limit.
Remember the one-liner.
The heap is only one part of a Node process's memory, so first find out which part is growing.
Coming next: How promises work inside. It shows the hidden records and the job queue that explain why promise callbacks run later.