I have a Node-RED instance that freezes several times a day. I instrumented the event loop and found something I can't explain: the blocking duration doubles almost exactly each round, reproducibly, across 12 independent cycles over two days.
I'm hoping someone here recognises the pattern, or can point me at a way to find which node is holding the loop.
The measurement
Added this to settings.js, above module.exports. It fires a 1-second timer and reports how late it actually ran — i.e. how long the event loop was blocked:
let __lagLast = Date.now();
setInterval(() => {
const now = Date.now();
const drift = now - __lagLast - 1000;
if (drift > 30) {
console.warn(`[lag] blocked ${drift}ms`);
}
__lagLast = now;
}, 1000);
Plus a memory sampler every 10 minutes:
setInterval(() => {
const m = process.memoryUsage();
const mb = (v) => (v / 1048576).toFixed(0);
console.log(`[mem] rss=${mb(m.rss)}MB heap=${mb(m.heapUsed)}/${mb(m.heapTotal)}MB`);
}, 600000);
The pattern
Every cycle after a restart looks like this:
~1100ms (startup, palette load — expected)
... quiet, sometimes for hours ...
~300-1300ms → ~3500ms → ~7000ms → ~15000ms → ~29000ms → ~60000ms → ~120000ms
Across 12 cycles spanning two days, same values every time:
| step | observations | range | spread |
|---|---|---|---|
| ~3500ms | 12 | 3156 – 3826 | ±10% |
| ~7000ms | 12 | 6650 – 7351 | ±5% |
| ~15000ms | 11 | 14447 – 15796 | ±5% |
| ~29000ms | 11 | 28707 – 30269 | ±3% |
| ~60000ms | 11 | 58377 – 63177 | ±4% |
| ~120000ms | 3 | 117564 – 121399 | ±2% |
Ratio between consecutive steps is consistently 1.9–2.2.
Once it reaches ~120s the add-on watchdog kills the container (SIGKILL, exit 137) and the sequence resets to zero on restart. On one run I disabled the watchdog to see how far it would go — 14 consecutive doublings, no convergence:
699 → 1057 → 3257 → 6954 → 14919 → 28629 → 58013 → 115173
→ 242000 → 479712 → 957443 → 2000600 → 4031923 ms
That last one is a single 67-minute event loop block. The process was alive the whole time and eventually recovered on its own — the editor came back after a few minutes on the shorter blocks.
The timing between the early steps is irregular (anywhere from 14 minutes to 3 hours), but once a block exceeds 15s it accelerates to the end within 30 seconds to 20 minutes.
The blocking comes before the disconnect, not after
This connects to a known open issue about the HA websocket palette freezing (zachowj/node-red-contrib-home-assistant-websocket#1990), where the assumption is that a websocket disconnect triggers the freeze. My data suggests the opposite:
23:06 restart, flows started
00:27 [lag] 124ms
00:27 [lag] 449ms
00:49 [lag] 993ms
00:49 [lag] 1039ms
00:53 [lag] 3790ms
00:53 [lag] 7164ms
01:01 [lag] 15485ms <- first block over 15s
01:01 Connection closed <- disconnect only appears HERE
There are zero disconnects during the first five doublings. The connection only drops once a block exceeds 15 seconds, which is exactly the HA Supervisor's PING/PONG timeout — the blocked event loop simply can't answer. So the disconnect looks like a symptom, not the cause.
Memory is not it
RSS sits flat at ~180MB for hours. It spikes during an episode and is fully reclaimed afterwards:
uptime 3.3h rss=226MB heap=80/102MB
uptime 3.5h rss=166MB heap=85/90MB
uptime 3.7h rss=171MB heap=85/90MB
uptime 3.8h rss=330MB heap=157/255MB <- during the 58s block
(next cycle) rss=180MB heap=77/83MB <- fully reclaimed
Heap never gets close to the limit. Whatever runs during an episode allocates heavily and is then collected.
Environment
| Host | Raspberry Pi 5 (8GB), Debian 12 bookworm, HA Supervised |
| Storage | NVMe SSD (no SD card) |
| Node-RED | v5.0.4 (HA add-on 22.0.2) |
| Node.js | v24.18.1 |
| Flows | 442 nodes across 10 tabs |
| Palette | node-red-contrib-home-assistant-websocket 0.80.3, node-red-dashboard 3.6.6 |
Already ruled out
Tested one variable at a time over about two weeks:
- Host resources —
vcgencmd get_throttled=0x0(never throttled, never under-voltage), CPU 42°C, 4.6GB RAM free, root on NVMe. - Flow structure — programmatic analysis of all 442 nodes: zero wire loops, zero recursion, no
setInterval/setTimeoutin function nodes, norepeatinjects, allapi-call-servicenodes set toqueue: none, only 3 entities both written and watched (all gated by trigger/delay). - Node count — disabled a whole tab (12
server-state-changedsubscriptions). Freeze frequency unchanged (~9/day before and after). - Context store —
localfilesystemmade single blocks worse (a 1.5MB global.json being reserialized on every write); reverted to memory. Doubling pattern identical either way. - The other side — Home Assistant's own log is completely silent at every disconnect timestamp, and HA stays fully responsive throughout. One integration blocking HA's executor was disabled; no effect on this.
- Websocket heartbeat — enabling it made things worse (disconnects 4–9/day → 18/day). Reverted.
unhandledRejectionhandler — stops the process from exiting on rejected promises, but has no effect on the freezing.
What I'm asking
-
What mechanism would make event-loop blocking double so precisely each round? My guess is something being registered twice as many times each cycle — listeners, subscriptions, timers — but I don't know how to confirm that from outside.
-
Is there a way to identify which node is holding the loop, without disabling flows? Disabling tabs is disruptive (this is a live home automation setup). I know about
logging.console.metrics: true— is that the best option, or is there a practical way to attach a CPU profiler to a running Node-RED instance?
Happy to run anything and report back. The lag monitor above is ten lines and makes the pattern obvious within a few hours.