cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Memory in Node.js

Learn how Node heap limits, memory counters, Buffers, heap snapshots, and out-of-memory reports help you keep servers healthy.

By the end, you can
  • 01
    Set realistic limitsExplain V8 heap limits and leave room below a container memory limit.
  • 02
    Read memory evidenceSeparate RSS, heap counters, external memory, and ArrayBuffer memory in a Node process.
  • 03
    Investigate 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.

Definition

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.

One browser-runnable megabytePop out in the code editor (opens in a new tab)JavaScript
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.

Small rule

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.

Read V8's heap-size limitJavaScript
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.

Set an old-space limitShell
node --max-old-space-size=64 app.js

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

Step through heap headroom
Step 0 of 4Ready
Your turn: follow the blue line

Replay of instrumented lesson code. It models why a Node heap flag must leave space below a container limit.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
const containerMB = 128;const headroomMB = containerMB - limitMB;console.log(headroomMB);
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
Real-life analogyLunch box and extra bag

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.

Print the memory-usage keysJavaScript
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.

The five memoryUsage counters
CounterPlain meaningUseful reminder
rssAll memory currently held by the Node process in RAM.It includes JavaScript heap, native code, stacks, and external allocations.
heapTotalV8 heap space currently reserved for JavaScript objects.It is capacity, not the amount of live JavaScript data.
heapUsedJavaScript heap space currently used by live objects and allocation work.It helps spot growing object graphs, but it misses Buffer bytes.
externalMemory used by native bindings that V8 accounts alongside the heap.Node Buffers commonly make this rise.
arrayBuffersThe 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.

Allocate a real 10 MB BufferJavaScript
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.

Step through Buffer accounting
Step 0 of 4Ready
Your turn: follow the blue line

Replay of an instrumented Buffer model. It explains accounting categories; it does not inspect browser memory.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
const bufferMB = 10;const after = { heapUsedMB: 3, externalMB: 11 };console.log(after.externalMB - before.externalMB);
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.
Playground: change a Buffer size
Node Buffer allocationJavaScript
const bytes = sizeMB * 1024 * 1024;const data = Buffer.alloc(bytes);console.log(data.length);
Modeled counters
heapUsed3 MB

Small JavaScript wrappers in this model.

external11 MB

Native Buffer bytes are counted here.

arrayBuffers10 MB

The Buffer payload is visible in this category.

Try it yourself

In this teaching model, a 10 MB Buffer makes external memory 11 MB and ArrayBuffer memory 10 MB while heapUsed stays 3 MB.

This is an accounting model. The Node-only example and test allocate a real 10 MB Buffer; browser sandbox code cannot use Node Buffer.

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.

Write a heap snapshot from codeJavaScript
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.

Ask Node to snapshot on SIGUSR2Shell
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.

Save one snapshot near the heap limitShell
node --max-old-space-size=64 --heapsnapshot-near-heap-limit=1 app.js

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

Deliberately exhaust a tiny V8 heapJavaScript
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.

Real-life analogyA full lunch box

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.

Common OOM causes and first responses
Likely causeWhat is happeningFirst next step
Unbounded array or cacheA collection keeps accepting entries and never drops old ones.Set a size limit, eviction policy, or expiry.
Huge JSON.parseOne very large string and its parsed object graph coexist briefly.Paginate, limit payloads, or use a streaming format when possible.
Whole-file loadingreadFile reads every byte before processing begins.Use a stream when the file can be processed piece by piece.
Large Buffer workloadBinary 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.

Where does this memory belong?
  • An ordinary cart object 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 rss value from process.memoryUsage().
Try it yourself
0 of 6 correct

Sort each card by the first accounting category you would inspect. The explanations name the useful counter or investigation.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

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

Exercise 1 · Warm-upPredict encoded bytes

Read the browser-runnable program and type its output.

Starter codePop out in the code editor (opens in a new tab)JavaScript
const bytes = new TextEncoder().encode("tea");
console.log(bytes.length);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upLeave container headroom

    What does this simple headroom model print?

    Starter codePop out in the code editor (opens in a new tab)JavaScript
    const containerMB = 128;
    const heapMB = 64;
    console.log(containerMB - heapMB);

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeName the Buffer clue

      A service allocates large Node Buffers. Which memoryUsage counter is your first clue?

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeOpen the evidence

        You have written a heap snapshot file. Where do you open it?

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeName a first OOM suspect

          Your service prints JavaScript heap out of memory. Name one first suspect from this lesson.

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeApply the lesson to uploads

            An upload service stores every active file in a Buffer. What should the service limit before traffic grows?

            Starter codePop out in the code editor (opens in a new tab)JavaScript
            const cache = new Map();
            cache.set("first", "tea");
            cache.set("second", "milk");
            cache.delete("first");
            console.log(cache.size);

            Answer, then press Check. Spacing and letter case don’t matter.

              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.

              Node memory quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What does --max-old-space-size=64 primarily limit?

                Choose an answer to see the explanation.

              2. Question 2 of 7What does this browser-runnable code print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const bytes = new ArrayBuffer(1024 * 1024);
                console.log(bytes.byteLength);

                Choose an answer to see the explanation.

              3. Question 3 of 7Which counter is the best first clue for a 10 MB Node Buffer?

                Choose an answer to see the explanation.

              4. Question 4 of 7Why should a container heap flag stay below the container memory limit?

                Choose an answer to see the explanation.

              5. Question 5 of 7Which Node command writes a snapshot when SIGUSR2 arrives?

                Choose an answer to see the explanation.

              6. Question 6 of 7What stable phrase identifies this V8 heap failure?

                Choose an answer to see the explanation.

              7. Question 7 of 7What does this bounded cache program print?

                Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
                const 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.

              CompleteFrontend Clear concepts. Working examples.