Finding Node.js memory leaks

Read process.memoryUsage(), recognise the handful of patterns that leak memory in bots and web apps, and find the culprit before an out-of-memory kill finds it for you.

A memory leak is code that keeps holding on to data it no longer needs, so your app's memory only ever grows. On a Falix server that ends one way: the container is killed when it exceeds its RAM (exit code 137), and your bot or site drops offline. This guide shows you how to measure Node's memory, spot the patterns that cause leaks, and narrow down the one in your code.

At a glance
You need A Falix server running the Node.js application
Plan Any — free shares 2.5 GB RAM, so leaks bite sooner; premium gives you your plan's RAM
Time Twenty minutes

First: is it actually a leak?

Memory that rises and then levels off is normal — Node grows its heap to a working size and stays there. A leak is memory that rises and never comes back down across hours of steady use. Watch the RAM card on the Console page: a slow, sawtooth-free climb toward your limit over hours is the tell. A one-time jump that plateaus is just your app warming up.

Reading process.memoryUsage()

Node hands you the numbers directly. Add this anywhere and check the console:

setInterval(() => {
  const m = process.memoryUsage();
  const mb = (n) => (n / 1024 / 1024).toFixed(1);
  console.log(`rss ${mb(m.rss)}MB | heapUsed ${mb(m.heapUsed)}MB | external ${mb(m.external)}MB`);
}, 60_000);

process.memoryUsage() returns five numbers, all in bytes:

Field What it measures Watch it when
rss Total resident memory the process holds — the number Falix compares to your limit Always; this is what triggers the OOM kill
heapUsed JavaScript objects currently alive Your own data structures leaking
heapTotal Heap V8 has reserved (≥ heapUsed) Rarely on its own
external Memory used by C++ objects bound to JS (buffers, some native modules) Native-module or buffer leaks
arrayBuffers The slice of external that is ArrayBuffer/Buffer data Streaming/file/image work

🎯 Good to know: rss is the one Falix enforces. If rss climbs steadily while heapUsed stays flat, the growth is outside the JS heap — look at buffers, streams, or a native module, not your objects.

The patterns that leak

Almost every Node leak is one of these. Each is something that keeps growing without a matching cleanup:

Pattern The leak The fix
Unbounded cache/Map You cache.set(key, value) forever and never delete old entries Cap it (evict oldest) or give entries a TTL
Listeners added in a loop emitter.on(...) / client.on(...) inside a handler that runs repeatedly, never removed Register listeners once at startup, or pair every on with an off/removeListener
Global arrays that only grow Pushing to a module-level logs = [] / history = [] and never trimming Keep the last N, or write to a database/file instead
Timers holding references setInterval closures that capture large objects and are never clearInterval'd Clear timers you no longer need
Per-request state on a shared object Storing request/message data on a long-lived singleton Keep request-scoped data local to the handler

For a Discord bot the classic version is a Map of per-guild or per-user state that you add to but never prune. For a web app it's a response cache with no size limit. Same shape, same fix: bound it.

⚠️ Heads up: Node warns about one of these for you. MaxListenersExceededWarning: Possible EventEmitter memory leak detected in the console almost always means you're adding listeners in a loop. Move the .on(...) call to run once.

Narrowing down the culprit

You don't have a full profiler on a panel host, but you rarely need one:

  1. Log memoryUsage() on a timer (the snippet above). Let it run and watch whether heapUsed or external is the one climbing — that tells you which kind of thing is leaking.
  2. Bisect by feature. Temporarily disable half your handlers/routes, restart, and watch. Whichever half keeps the climb contains the leak. Repeat on that half. This finds most leaks faster than any tool.
  3. Count your collections. Log myMap.size / myArray.length next to the memory line. A number that only ever goes up is your leak.

A bounded cache is the single most common fix — here's the shape:

const cache = new Map();
const MAX = 500;

function remember(key, value) {
  if (cache.size >= MAX) {
    cache.delete(cache.keys().next().value); // drop the oldest
  }
  cache.set(key, value);
}

The --max-old-space-size reality

You may read that raising Node's heap limit fixes memory crashes. Two honest points for Falix:

  • It only delays a real leak — the memory still grows, just to a higher ceiling before the same OOM kill.
  • That flag has to go before the filename in the node command, which means editing the full startup command — and that's premium-only. The Additional Arguments (NODE_ARGS) variable is appended after your filename, so it reaches your script, not node. Don't expect NODE_ARGS to set node flags.

Fix the leak instead of raising the ceiling; it's the only change that actually holds.

Troubleshooting

  • Killed with exit code 137 — that's the out-of-memory kill. Confirm with the RAM card, then hunt the leak above. See Out of memory.
  • Memory high but stable — probably not a leak; it's your working set. If it's simply more than your plan's RAM, you need less data in memory (stream, paginate, use a database) or more RAM.
  • external climbing, heap flat — buffers or a native module. Look at file/image/stream code that isn't releasing buffers.

For the full toolset (heap snapshots, the --inspect protocol, v8.getHeapStatistics()), the official Node.js diagnostics guide takes it further than a panel host can.


Next steps

Was this guide helpful?