Why Your Node.js App Suddenly Uses 2GB of RAM
Learn how to diagnose and fix unexpected memory spikes in Node.js apps with heap snapshots and practical code examples.
The 2GB mystery
Your Node.js app was humming along at 200MB of RAM. Then, after a deploy, it climbs to 2GB and stays there. The process doesn't crash, but your server bill does. This is a common story, and the fix usually isn't a one-liner.
In this post, I'll walk through a systematic approach to find the culprit: from heap snapshots to event loop analysis. I'll use a real example that exhibits the classic 'accidental closure' leak, and I'll show you how to reproduce, diagnose, and fix it.
You'll leave with a set of commands and code snippets you can apply to your own app today.
- Understand the difference between heap and RSS memory.
- Use --inspect and Chrome DevTools to take heap snapshots.
- Identify common leak patterns like closures and timers.
- Implement a simple memory monitoring script.
Before you start
You need Node.js 18 or later and a terminal. I'll use a minimal Express app as the example, but the techniques apply to any Node process.
Create a new directory and a package.json with npm init -y. Then install express.
mkdir memory-leak-demo && cd memory-leak-demo
npm init -y
npm install expressStep 1: Reproduce the leak
Here's a minimal Express app that leaks memory. It creates a closure that captures a large array and never lets it go. The leak is subtle: the array is referenced by a function that is stored in a global cache.
Create app.js with the following code.
const express = require('express');
const app = express();
const cache = {};
app.get('/leak', (req, res) => {
const bigArray = new Array(1e6).fill('x');
cache[Date.now()] = () => bigArray; // closure holds reference
res.send('Leaked');
});
app.listen(3000, () => console.log('Server on port 3000'));Step 2: Run the app and hit the endpoint
Start the server in one terminal. In another, use a loop to call the /leak endpoint 100 times. Each call adds a new entry to the cache, and each entry holds a 1M-element array.
Watch the memory usage with the ps command or a monitoring script.
node app.jsfor i in {1..100}; do curl -s http://localhost:3000/leak > /dev/null; done
ps -o rss,cmd -p $(pgrep -f 'node app.js')Step 3: Take a heap snapshot
The command-line approach works for quick checks, but to really see the leak, you need a heap snapshot. Node has a built-in inspector. Start the app with --inspect, then open Chrome DevTools.
Alternatively, use the v8 module to take a snapshot programmatically. Here's a script that triggers a snapshot after a few requests.
const v8 = require('v8');
const fs = require('fs');
// after some requests, save snapshot
setTimeout(() => {
const snapshot = v8.writeHeapSnapshot();
console.log('Snapshot saved to', snapshot);
}, 5000);Step 4: Analyze the snapshot
Open the snapshot file in Chrome DevTools (Profiles > Load). Look for the 'Closure' or 'Array' entries. You'll see hundreds of arrays, each 8MB. The retaining path shows the closure holds the array, and the cache holds the closure.
This confirms the leak: the cache never clears entries. In a real app, the leak might be in a database driver, a logging library, or your own code.
- Look for 'Array' objects with large shallow sizes.
- Check the 'Retainers' panel to see what holds them.
- Common culprits: global variables, timers, event listeners, closures.
Step 5: Fix the leak
The fix depends on the cause. For our demo, we simply clear the cache or avoid storing closures. In real code, you might need to null out references, use WeakMap, or clear intervals.
For the demo, change the /leak route to not store the closure, or add a cleanup function.
app.get('/leak', (req, res) => {
const bigArray = new Array(1e6).fill('x');
res.send('OK'); // no closure stored
});Step 6: Verify the fix
Restart the server and repeat the stress test. Memory should stay flat. Use the same ps command or a monitoring script to confirm.
If memory still grows, take another snapshot and repeat the analysis. Sometimes the leak is in a dependency.
node app.js
# in another terminal
for i in {1..100}; do curl -s http://localhost:3000/leak > /dev/null; done
ps -o rss,cmd -p $(pgrep -f 'node app.js')What I would do in production
For a production app, I would add memory monitoring and alerting. Here's a simple script that logs RSS and heap usage every minute. You can run it with cron or a process manager.
Also, I would enable the --inspect flag only in staging, not production, for security.
Set up a heap snapshot on demand for when the alert fires.
const v8 = require('v8');
const os = require('os');
setInterval(() => {
const rss = process.memoryUsage().rss / 1024 / 1024;
const heap = v8.getHeapStatistics().used_heap_size / 1024 / 1024;
console.log(`RSS: ${rss.toFixed(2)} MB, Heap: ${heap.toFixed(2)} MB`);
if (rss > 1024) {
v8.writeHeapSnapshot();
console.error('Memory warning: snapshot saved');
}
}, 60000);Troubleshooting
If your app grows memory but no snapshot shows a clear leak, consider these possibilities:
Event loop starvation can cause memory to grow because callbacks queue up. Check with a simple event loop delay script.
Native modules can leak outside the V8 heap. Use process.memoryUsage() to see external memory.
Sometimes the leak is in the garbage collector itself, but that's rare.
setInterval(() => {
const start = Date.now();
setImmediate(() => {
const delay = Date.now() - start;
if (delay > 50) console.log('Event loop delay:', delay);
});
}, 100);- Check event loop delay with a setInterval that runs every 100ms and logs the delay.
- Run with --trace-gc to see GC logs.
- Use process.memoryUsage() to see rss, heapTotal, heapUsed, and external.
FAQ
- Q: What's the difference between RSS and heap? A: RSS is the total physical memory used by the process, including native code and buffers. Heap is the V8-managed memory for JavaScript objects.
- Q: Can I set a memory limit for Node? A: Yes, use --max-old-space-size to cap the heap, but it's better to fix the leak.
- Q: Why does my app use 2GB but not crash? A: Node's default heap limit is about 2GB on 64-bit. It may be close to the limit but not exceeding it.
- Q: How often should I take heap snapshots? A: Take one at startup and one when memory is high, then compare.
Key takeaways
- Apply one concrete change from this post before collecting more reading.
- Prefer browser-side tools when the work involves secrets, tokens, or PII.
- Document the why next to the how so the next reviewer inherits context.
FAQ
- Who is this guide on nodejs for?
- Working developers who need a practical take on why your node.js app suddenly uses 2gb of ram — not a marketing overview. Skim the sections, apply one tip, then come back when you hit an edge case.
- Do I need an account to use the related tools?
- No. code.live tools run in your browser with no signup. Nothing you paste is uploaded to a server for the client-side utilities linked from this post.
- How often is this article updated?
- This post was published September 5, 2026. Fundamentals stay stable; check linked tool pages and official docs when version-specific behavior matters.