Diagnosing Node-RED Memory Leaks and CPU Spikes
A diagnostic guide for Node-RED memory leaks and CPU spikes: identify symptoms, audit Function nodes for global variables and orphaned intervals, throttle high-frequency triggers, and verify fixes with heap monitoring.
04 Apr 2026, 21:32 UTC

When a Node-RED instance becomes unresponsive, exhibits high latency, or crashes with an 'Out of memory' error, the bottleneck is rarely the hardware itself. Instead, because Node-RED runs on a single-threaded Node.js event loop, a single inefficient flow or an unmanaged variable can starve every other process running on the instance.
This guide helps you distinguish between CPU-bound bottlenecks and memory-bound exhaustion, providing specific diagnostic steps to stabilize your runtime.
Identifying the Symptom
Before diving into code, match your observed behavior to the most common root causes:
| Symptom | Likely Cause | Impact |
|---|---|---|
| CPU at 100%; UI frozen; MQTT/API messages lag | Synchronous code in Function nodes | Total runtime blockage for all flows |
| Steady memory growth (Heap increase) | Global variables or uncleared intervals | Eventual crash (OOM) |
| Sudden memory spikes | High-frequency JSON payloads | Immediate instability/GC pressure |
| Memory never drops after traffic stops | Unresolved Promises or hanging callbacks | Resource exhaustion over time |
Step 1: Quantifying Memory Usage
To confirm you have a memory leak, you must monitor the Node.js heap. You can do this by adding a temporary Function node to your flow to log memory statistics to the console.
Attach an Inject node (set to every 5 seconds) to a Function node with the following code:
const memory = process.memoryUsage();
msg.payload = {
rss: (memory.rss / 1024 / 1024).toFixed(2) + "MB",
heapUsed: (memory.heapUsed / 1024 / 1024).toFixed(2) + "MB",
heapTotal: (memory.heapTotal / 1024 / 1024).toFixed(2) + "MB"
};
return msg;
Check: If heapUsed increases continuously and never returns to a baseline after stopping the input, you have a memory leak.
Step 2: Auditing Function Nodes
Most leaks in Node-RED occur within the Function node. Check your nodes for these three specific patterns:
1. Global Variable Accumulation
Variables defined outside the on:msg function persist across deployments. If you push data to an array defined here, it will grow until the process crashes.
// BAD: This array grows forever
let myData = [];
myData.push(msg.payload);
// GOOD: Use context with a limit or external storage
let data = context.get('data') || [];
data.push(msg.payload);
context.set('data', data);
2. Orphaned Intervals and Event Listeners
If you use setInterval or addEventListener, you must clear them when the node is stopped or redeployed. If you don't, the interval continues running in the background every time you hit 'Deploy'.
Fix: Use the stop method in the Function node to clear resources.
module.stop = function(node) {
clearInterval(node.myInterval);
};
3. Large Payload Copying
Node-RED passes the entire msg object between nodes. If you pass a 10MB JSON object through 10 nodes, you are effectively creating massive memory overhead. Consider using the change node to drop unnecessary properties as early as possible in the flow.
Step 3: Resolving CPU Spikes
If your CPU is pegged, the event loop is blocked. Since Node.js is single-threaded, a while loop or a heavy synchronous calculation in a Function node will stop all other nodes from processing messages.
- High-Frequency Triggers: If an MQTT sensor is sending 100 messages per second, use a
ratenode or adelaynode in rate-limit mode to throttle input before it reaches a Function node. - Synchronous Heavy Lifting: Move CPU-intensive work (encryption, image processing, large JSON parsing) to a separate Node.js worker thread via a custom node, or offload to an external service via HTTP/MQTT.
- Debug Node Overhead: Disable debug nodes with "complete msg object" enabled in production; serializing large objects for the sidebar consumes significant CPU cycles.
Step 4: Context Storage Strategy
The context object (flow, global, local) uses memory by default. For large datasets or long-running deployments:
- Configure
settings.jsto use file-based or Redis-backed context storage instead of the default memory store. - Implement TTL (time-to-live) logic: store a timestamp with each entry and purge stale data on write.
- Avoid storing session-specific data in
globalcontext; it persists across redeploys unless explicitly cleared.
Verification Checklist
After applying fixes, run this validation sequence:
- Deploy the memory monitor Function node (Step 1).
- Generate load using an Inject node at expected peak frequency for 5 minutes.
- Stop the Inject node and observe heap for 2 minutes.
- Pass:
heapUsedstabilizes within 10% of pre-load baseline. - Fail: Heap continues climbing or plateaus significantly above baseline — re-audit Function nodes for uncleared intervals or growing arrays.
Escalation Criteria
If memory stabilizes but CPU remains high under normal load, or if the process crashes with FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory despite fixes, escalate to:
- Profiling with
node --inspectand Chrome DevTools to capture CPU profiles and heap snapshots. - Reviewing custom nodes for native addon memory leaks (C++ bindings).
- Increasing Node.js heap limit via
NODE_OPTIONS=--max-old-space-size=2048(temporary mitigation only).
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.