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 realistic limitsExplain V8 heap limits and leave room below a container memory limit.
- 02Read memory evidenceSeparate RSS, heap counters, external memory, and ArrayBuffer memory in a Node process.
- 03Investigate failuresCapture a heap snapshot and turn a JavaScript heap out-of-memory crash into a focused next step.
Memory in a Node process
A Node server runs JavaScript, but it also runs V8, Node native code, network libraries, file work, and sometimes binary data. Looking only at JavaScript objects can therefore tell an incomplete story. A healthy memory investigation starts by naming which kind of memory is growing.
Node process memory is all memory held by one running Node program. V8's heap is one important part; Buffers and native work can use memory outside that heap.
This lesson continues from Memory in large apps. That lesson asks whether an app keeps data too long. Here, the server question is also practical: can this process stay below its machine or container memory limit while handling real requests?
We will read counters, choose a heap limit, make a snapshot, and recognize an out-of-memory report. These tools do not replace ordinary ownership decisions. They make the next investigation small enough to do calmly.
Bytes before memory counters
A byte is a small unit of storage. Counters such as external are reported in bytes, so it helps to see one familiar amount before reading a server report.
const bytes = new ArrayBuffer(1024 * 1024);console.log(bytes.byteLength);Line 1 creates an ArrayBuffer with 1024 times 1024 bytes. Line 2 prints its length. The real output is 1048576, which is one mebibyte expressed as bytes. The example runs in a browser, so it connects the number to code you can edit here.
Server tools often show a large number such as 10485760. Do not guess from the digits. Divide by 1024 twice when you need a rough megabyte value, and compare the same counter over time rather than treating one sample as a verdict.
Memory values move while a process works. A useful observation is a repeatable direction, such as external memory growing after each binary upload, not one exact number from one machine.
Heap limits and containers
V8's heap limit is the maximum managed JavaScript heap V8 is willing to grow toward. Node exposes the current limit through v8.getHeapStatistics().heap_size_limit. The value changes when you start Node with a different old-space limit.
const v8 = require("node:v8");console.log(v8.getHeapStatistics().heap_size_limit);Line 1 loads Node's built-in V8 module. Line 2 prints the current heap limit in bytes. This is Node-only code, so the lesson does not offer it to the browser editor. The lesson test starts child processes with 32 MB and 64 MB flags and proves only that the 64 MB process reports a larger limit.
node --max-old-space-size=64 app.jsThe flag is measured in megabytes and constrains V8 old space. It is useful for making a service fail nearer to an intentional boundary, but it is not a whole-process fence. RSS can still include native code, stacks, code pages, and Buffer payload bytes.
Replay of instrumented lesson code. It models why a Node heap flag must leave space below a container limit.
script
const containerMB = 128;const headroomMB = containerMB - limitMB;console.log(headroomMB);Think of the V8 heap as a lunch box with a fixed size. A Buffer is like carrying extra food in a separate bag: it is not inside the lunch box, but you are still carrying it.
- In real life: A lunch box has a fixed capacity
- In JavaScript: V8 heap has a configured limit
- In real life: Extra food can go in a separate bag
- In JavaScript: Buffers use external memory
- In real life: You still carry both containers
- In JavaScript: RSS includes heap and external memory
- In real life: A small bag does not expand the lunch box
- In JavaScript: External bytes do not become heapUsed
Where the analogy stops: Real memory accounting has more categories than two bags, and operating systems can reclaim or reserve pages differently.
In a container, set the heap flag below the container's memory limit. The difference is headroom for everything that is not ordinary V8 heap data. Without it, the operating system may kill the whole process before V8 can give you a useful heap failure.
Reading process.memoryUsage()
Memory usage counters are a snapshot of categories Node can report for its current process. Call process.memoryUsage() when you want those categories. Read them together; no one field tells the whole memory story.
const usage = process.memoryUsage();console.log(Object.keys(usage).join(","));Line 1 asks Node for a fresh usage object. Line 2 prints its property names. The real child-process output is rss,heapTotal,heapUsed,external,arrayBuffers. The order matters here because it makes the five counters easy to find in a log.
| Counter | Plain meaning | Useful reminder |
|---|---|---|
| rss | All memory currently held by the Node process in RAM. | It includes JavaScript heap, native code, stacks, and external allocations. |
| heapTotal | V8 heap space currently reserved for JavaScript objects. | It is capacity, not the amount of live JavaScript data. |
| heapUsed | JavaScript heap space currently used by live objects and allocation work. | It helps spot growing object graphs, but it misses Buffer bytes. |
| external | Memory used by native bindings that V8 accounts alongside the heap. | Node Buffers commonly make this rise. |
| arrayBuffers | The portion associated with ArrayBuffers and Node Buffers. | It is useful for seeing binary data separately from ordinary objects. |
rss is all memory currently held by the Node process in RAM. heapTotal is V8 heap capacity currently reserved for JavaScript objects. heapUsed is JavaScript heap space currently in use. external is native memory V8 accounts alongside the heap. arrayBuffers is the part associated with ArrayBuffers and Node Buffers.
Sample at meaningful boundaries: before a batch, after a batch, and after work should be released. Logging every request can create noise and can itself retain strings. For a long-running service, add a small metrics series with the same five counters.
Buffers and external memory
A Buffer is Node's convenient binary-data type. It is useful for files, network payloads, images, and cryptographic data. Its payload is not an ordinary JavaScript object graph, so a large Buffer can raise external memory far more than heapUsed.
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 counters before allocation. Line 2 makes a ten-megabyte Buffer. Line 3 gets a second snapshot. Lines 4 through 6 print facts about the allocation. The child-process test confirms that external grows by at least 10 MB while heapUsed grows much less; it never assumes an exact platform-specific counter value.
Replay of an instrumented Buffer model. It explains accounting categories; it does not inspect browser memory.
script
const bufferMB = 10;const after = { heapUsedMB: 3, externalMB: 11 };console.log(after.externalMB - before.externalMB);const bytes = sizeMB * 1024 * 1024;const data = Buffer.alloc(bytes);console.log(data.length);3 MBSmall JavaScript wrappers in this model.
11 MBNative Buffer bytes are counted here.
10 MBThe Buffer payload is visible in this category.
In this teaching model, a 10 MB Buffer makes external memory 11 MB and ArrayBuffer memory 10 MB while heapUsed stays 3 MB.
A Buffer is not free simply because it is outside the V8 heap. Keep binary work bounded: limit concurrent uploads, avoid collecting every file in an array, and let each completed request drop its references. Watch RSS with external memory when binary workloads grow.
Heap snapshots in Node.js
A heap snapshot is a file describing objects V8 sees in its JavaScript heap at one moment. It is most useful when you compare before-and-after snapshots or inspect what retains a collection that should have disappeared.
const v8 = require("node:v8");const filename = v8.writeHeapSnapshot();console.log(filename);Line 1 loads Node's V8 helpers. Line 2 writes a snapshot file and prints its filename. Run this deliberately in a safe environment because making a snapshot needs memory and can pause work. Open the resulting file in Chrome DevTools' Memory panel.
node --heapsnapshot-signal=SIGUSR2 app.js
kill -USR2 <pid>The first line starts Node with a signal handler for snapshots. The second line sends SIGUSR2 to the process. This is handy when code cannot easily reach the problem moment itself, but use a process supervisor and deployment rules appropriate for your service.
node --max-old-space-size=64 --heapsnapshot-near-heap-limit=1 app.jsThis command asks Node to write one snapshot when V8 nears its configured heap limit. It is an emergency clue, not a cure. For reading the result, continue with Heap snapshots in depth and look for retaining paths.
Diagnosing out-of-memory crashes
An out-of-memory crash happens when V8 cannot grow the JavaScript heap enough for the next allocation. A typical report contains FATAL ERROR: ... JavaScript heap out of memory. Read that phrase first; it identifies a V8 heap problem, not every possible reason a container stopped.
const orders = [];while (true) { orders.push("tea ".repeat(10000));}Line 1 keeps an array reachable. Line 3 keeps adding long strings forever. Run this only in an isolated child process with a tiny limit. The lesson test uses --max-old-space-size=32, verifies a non-zero exit code, and checks stderr for heap out of memory.
An out-of-memory crash is like stuffing a lunch box after it is full. The lid will not close, so Node stops instead of pretending the new food fits.
- In real life: Food keeps being added
- In JavaScript: The array keeps retaining strings
- In real life: The lunch box is already full
- In JavaScript: V8 has reached its heap limit
- In real life: The lid will not close
- In JavaScript: The next allocation cannot fit
- In real life: Packing stops
- In JavaScript: Node stops with a fatal error
Where the analogy stops: V8 also collects garbage and tries several allocation strategies; the analogy only explains why a hard limit eventually stops growth.
| Likely cause | What is happening | First next step |
|---|---|---|
| Unbounded array or cache | A collection keeps accepting entries and never drops old ones. | Set a size limit, eviction policy, or expiry. |
| Huge JSON.parse | One very large string and its parsed object graph coexist briefly. | Paginate, limit payloads, or use a streaming format when possible. |
| Whole-file loading | readFile reads every byte before processing begins. | Use a stream when the file can be processed piece by piece. |
| Large Buffer workload | Binary bytes raise external memory and RSS. | Limit concurrent work and release references when each job ends. |
Use a short checklist: confirm whether the report says JavaScript heap out of memory, inspect heap and external counters, reproduce with a safe limit, capture a snapshot if possible, and find the collection or retaining path that grows. Raising a limit can buy time, but it hides an unbounded array or cache only temporarily.
A small server memory routine
Start with a budget. Know the container memory limit, choose a smaller V8 old-space limit, and leave room for native work. Then choose a workload boundary, such as an upload batch or a report request, where you can compare counters before and after.
Keep collections bounded. A cache needs a maximum size or expiration. A file pipeline should stream when it can process pieces in order. A JSON endpoint needs a request size limit before one request turns into a huge string and parsed object graph 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 native database binding.
- Thread stacks and Node native code.
- The
rssvalue from process.memoryUsage().
Sort each card by the first accounting category you would inspect. The explanations name the useful counter or investigation.
Alert on a trend and tie it to a workload. A rising heap after each completed job suggests retained JavaScript objects. Rising external memory during image work suggests binary concurrency. Rising RSS with neither clue deserves a closer look at native libraries and process behavior.
Common mistakes
- “heapUsed is all process memory.” It is only V8-managed JavaScript heap use; RSS is broader.
- “A Buffer cannot cause memory pressure.” Buffer payload bytes are external, but they still raise process memory.
- “The heap flag equals the container limit.” The process needs headroom beyond old space.
- “A larger heap fixes the bug.” It can postpone a crash while an unbounded collection continues growing.
- “A heap snapshot shows every native byte.” It focuses on V8 heap objects, not all RSS categories.
The useful contrast is simple. Heap tools explain retained JavaScript objects. Process counters explain broader pressure. You often need both: a snapshot to find a retained array and RSS plus external counters to understand a binary-heavy service.
Practice exercises
Read the browser-runnable program and type its output.
const bytes = new TextEncoder().encode("tea");
console.log(bytes.length);It prints 3. The ASCII letters in tea each use one UTF-8 byte.
What does this simple headroom model print?
const containerMB = 128;
const heapMB = 64;
console.log(containerMB - heapMB);It prints 64. This is a planning model for the space kept outside V8 old space.
A service allocates large Node Buffers. Which memoryUsage counter is your first clue?
Watch external and arrayBuffers. A Buffer can be large even when heapUsed changes only a little.
You have written a heap snapshot file. Where do you open it?
Open the file in Chrome DevTools' Memory panel, then inspect summaries and retaining paths.
Your service prints JavaScript heap out of memory. Name one first suspect from this lesson.
Start with an unbounded array or cache, a huge JSON.parse, or loading a whole file. Measure before changing the limit.
An upload service stores every active file in a Buffer. What should the service limit before traffic grows?
const cache = new Map();
cache.set("first", "tea");
cache.set("second", "milk");
cache.delete("first");
console.log(cache.size);Limit concurrent uploads and enforce a file-size limit. That bounds Buffer payloads and makes RSS and external memory more predictable.
Check your understanding
Use the counters as categories, not as magic numbers. Then choose a bounded workload and a tool that can test your first guess.
Question 1 of 7What does
--max-old-space-size=64primarily limit?Choose an answer to see the explanation.
Question 2 of 7What does this browser-runnable code print?
Read the code, then predictconst bytes = new ArrayBuffer(1024 * 1024); console.log(bytes.byteLength);Choose an answer to see the explanation.
Question 3 of 7Which counter is the best first clue for a 10 MB Node Buffer?
Choose an answer to see the explanation.
Question 4 of 7Why should a container heap flag stay below the container memory limit?
Choose an answer to see the explanation.
Question 5 of 7Which Node command writes a snapshot when SIGUSR2 arrives?
Choose an answer to see the explanation.
Question 6 of 7What stable phrase identifies this V8 heap failure?
Choose an answer to see the explanation.
Question 7 of 7What does this bounded 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
- V8 heap limits constrain managed JavaScript heap, not all process memory.
- Set old-space below a container limit so RSS has room for external and native memory.
- Read rss, heapTotal, heapUsed, external, and arrayBuffers together over a workload.
- Buffers commonly raise external and arrayBuffers far more than heapUsed.
- Snapshots explain retained heap objects; process counters explain broader memory pressure.
- For an OOM, find the growing collection or workload before simply raising a limit.
Remember the one-liner.
A Node heap limit is one part of process memory, so investigate the category that is actually growing.
Coming next: How promises work inside, where reaction records and jobs explain why promise callbacks run later.