# RSS grows in 20–30 MB steps between event-loop ticks, and the first hours after start decide the floor — is this known?

**URL:** <https://community.homey.app/t/rss-grows-in-20-30-mb-steps-between-event-loop-ticks-and-the-first-hours-after-start-decide-the-floor-is-this-known/158723>\
**Category:** Developers\
**Tags:** app, homey-pro, homey-pro-2023\
**Created:** [August 25, 2026, 3:41am UTC](https://community.homey.app/t/rss-grows-in-20-30-mb-steps-between-event-loop-ticks-and-the-first-hours-after-start-decide-the-floor-is-this-known/158723 "2026-08-25T03:41:06Z")\
**Posts on this page:** 20\
**Page:** 1

<div class="post-metadata">

**Author:** ![melectro](https://sea1.discourse-cdn.com/flex025/user_avatar/community.homey.app/melectro/32/78999_2.png) [@melectro](https://community.homey.app/u/melectro)\
**Post date:** [August 25, 2026, 3:41am UTC](https://community.homey.app/t/rss-grows-in-20-30-mb-steps-between-event-loop-ticks-and-the-first-hours-after-start-decide-the-floor-is-this-known/158723/1 "2026-08-25T03:41:06Z")

</div>

Homey Pro (Early 2023) · two private apps, one simple and one complex, both affected

### Summary

An app of mine is killed by `Memory Warning Limit Reached` every few hours. I  
have instrumented it heavily and can now describe the behaviour precisely. Two  
findings that I have not seen written down anywhere:  
RSS grows in discrete 20–33 MB steps, and page-fault counters read  
immediately before and after each loop iteration place them between two  
iterations, not inside one. Most are returned within minutes; occasionally  
one is not, and the floor ratchets up permanently.  
The event-loop rate during the first 8–10 hours after start determines  
where RSS settles. Polling every 500 ms from a cold start reaches 160 MB  
in 70 minutes and gets killed. Running the first ten hours at 1500 ms and  
only then switching to 500 ms settles the floor at ~100–120 MB — and after  
that the interval makes no difference at all. The floor stops rising about  
9 hours in, which is where the ten hours comes from.  
The two are independent: the steps arrive at the same size and roughly the same  
rate whatever the loop interval. What the interval changes is the floor they  
land on, and therefore whether a step is survivable.  
A second app on the same Homey, with unrelated code and a very different  
workload, shows steps of the same shape. I have not been able to test what  
would remove them from either app.  
How RSS is measured here. An app cannot read its own current RSS (see  
Q5 — Can an app read its own RSS? below), so a separate monitoring app polls  
`usage.mem` for each app through homey-api every 15 seconds. All floor and step figures below come from  
that. Figures called `maxRSS` come from `process.resourceUsage()` inside the  
app and are high-water marks.

### The apps

#### App 1

— energy management for a Sungrow SH10RT hybrid inverter with a  
25.6 kWh battery. Modbus TCP polling, a state machine for charge/discharge,  
~76 capabilities, one device, no cloud, one persistent TCP socket.

#### App 2

— draws a PNG chart every five minutes and uploads it over HTTP (to my internal web server on LAN) .  
Reads app 1’s capabilities via homey-api. Around 3 MB of preallocated buffers  
and almost no logic.

### What the kill looks like

```auto
12:44:16 app starts, floor 85 MB
13:00 warm-up done, floor 97.4
13:00-21:00 drift 1.17 MB/h floor 106.8
21:48:39 +33.0 MB, single step -> 143.6 not returned
21:50-22:44 145.7 flat for nearly an hour
22:44:11 loop interval 1500 -> 500 ms
22:46:56 +4.3 MB -> 150.1
22:48:09 killed

```

That matches the documented behaviour — warning above 150 MB, killed after  
about 50 seconds above it — so the kill itself works as designed. The question  
is where the 33 MB came from.  
The drift between steps is 1.17 MB/h. At that rate the run would have taken  
30 hours to reach the limit on its own; it was killed after 10, one hour after a  
single unreturned step.

### The steps: measured over 24 hours

Eight increases of ≥8 MB, measured against actual RSS sampled every 15 seconds  
by a separate monitoring app:

```auto
time floor -> peak +MB outcome
08:38:29 103.8 -> 129.5 +25.7 returned after 6.0 min
09:15:35 99.2 -> 114.1 +14.9 returned after 3.0 min
12:20:51 109.5 -> 132.1 +22.6 PERMANENT
14:28:26 104.0 -> 129.2 +25.2 returned after 4.3 min
19:53:20 105.3 -> 117.3 +12.0 returned after 2.0 min
21:48:39 110.6 -> 143.6 +33.0 PERMANENT -> killed one hour later
00:48:38 100.6 -> 121.1 +20.5 returned after 6.5 min
01:42:02 101.2 -> 128.2 +27.0 returned after 1.3 min

```

Six of eight are returned to the OS within minutes. Two are not, and those lift  
the floor for good. Size does not predict which: 22.6 MB stayed while  
27.0 MB was returned. Neither does `peak_malloced_memory`.  
Between the steps the floor drifts 1.2 MB/h.

### Where the memory is allocated

I read `process.resourceUsage().minorPageFault` immediately before and  
immediately after each poll cycle, so an increase can be attributed to one side  
or the other. Every step in the 24-hour window looks the same:

```auto
06:14 between ticks 26.6 MB gap 1442 ms
08:38 between ticks 25.5 MB gap 1446 ms
11:31 between ticks 24.8 MB gap 1433 ms
13:18 between ticks 22.7 MB gap 1455 ms
14:28 between ticks 23.5 MB gap 1388 ms
18:42 between ticks 25.1 MB gap 1430 ms
19:53 between ticks 30.2 MB gap 1442 ms
21:48 between ticks 33.1 MB gap 1456 ms
23:38 between ticks 26.6 MB gap 1435 ms
01:41 between ticks 25.5 MB gap 1419 ms

```

Always between two iterations, always across a 1388–1456 ms interval — the  
configured 1500 ms less the iteration’s own run time — and always 22–33 MB. The measurements taken inside a tick in the same seconds are  
1.6–3.8 MB. None of my JavaScript is running when the 25 MB arrives.  
Page faults track the size exactly — in a control test, allocating a 30 MB  
Buffer produced 7689 minor page faults (30.0 MB).  
The gap length also identifies the loop interval, which settles a question I  
expected to be asked. Of twelve measured events in that 24-hour window, eleven  
happened at a 1388–1456 ms gap (the 1500 ms interval) and exactly one at 448 ms  
(the 500 ms interval). The steps are therefore not a consequence of running  
fast — the largest one, 33 MB, arrived nine hours into a slow-polling window.

### Everything V8 reports stays flat

Over a 4-hour run RSS went from 92 to 160 MB while all V8 figures together grew  
about 6 MB:  
metric start after 4 h  
`heapUsed` 18.4 MB 23.0 MB  
`heapTotal` 23.2 MB 27.9 MB  
`external` 3.8 MB 3.8 MB  
`malloced_memory` 0.5 MB 0.5 MB  
active libuv handles Pipe:1, Socket:4 unchanged  
maxRSS 92.1 MB 160.2 MB  
Handle counts never move, so it is not a handle or socket leak.

### The warm-up rate decides the floor

One process, 71 hours, no restarts, two variables changed at known times.  
Poll interval 1500 ms → 500 ms, while already settled

```auto
5 h before (1500 ms): floor 121.5 MB
3 h after (500 ms): floor 121.5 MB difference +0.0 MB

```

For comparison, from a cold start at 500 ms the first ≥5 MB step arrived after  
12–16 minutes in all four runs I have, and it was 22–27 MB every time.  
Cold start, same code, same hardware, same day

```auto
500 ms throughout 94 -> 160 MB in 70 minutes -> killed
1500 ms for 8-10 h 97 -> 121 MB over ~9 hours -> flat, ran 71 hours
   then 500 ms no effect at all

```

During warm-up the growth tracks the number of loop iterations rather than  
wall-clock time: roughly 25–30 MB per 10 000 iterations at both 500 ms and  
1000 ms. Halving the interval does not make an iteration cost more — it just  
runs twice as many per hour, so the ceiling arrives twice as fast.  
I now start the app at 1500 ms and switch to 500 ms after ten hours. It keeps  
the floor around 100 MB instead of 125, which buys margin — but it does not stop  
the steps, and it does not always save the app. In the run shown above the floor  
had drifted to 110.6 when a 33 MB step landed, and that was enough.

### Forcing garbage collection helps during warm-up, not after

Same poll interval (1500 ms, verified at 10 iterations per 15 s), same code,  
same machine, same warm-up phase — the only difference is `global.gc()` every  
30 seconds:

```auto
GC off 89.9 -> 128.7 MB in 1.7 h = 23.3 MB/h -> killed after 1 h 58 min
GC on 96.8 -> 109.3 MB in 1.7 h = 7.5 MB/h -> settled, ran for days

```

But GC does not reclaim a step once it has happened. In the 70 seconds  
between the fatal step and the kill, five garbage collections ran and RSS did  
not move a single MB. A 20 MB transient on another day took 5 minutes 16  
seconds to return with GC running every 30 seconds throughout — so whatever  
returns the memory operates on a minutes timescale and is not GC.

### A second app shows the same pattern

App 2 does almost nothing: renders a PNG every five minutes, uploads it, reads  
capabilities. Over 22 hours its floor moved 93.9 → 97.7 MB, with two transients  
(+9.2 and +11.7 MB) that returned within five minutes. On another day it took  
two ~10 MB steps that did not return, then sat flat for 14 hours.  
Same stepping shape, completely different code. That is what makes me think  
this is platform behaviour rather than something in my control logic.

### Questions

Q1 — Is the warm-up effect known?  
Is it known that a faster event-loop cadence during the first hours after start  
leads to a permanently higher RSS floor on the 2023+ hardware? It is not  
intuitive: an iteration that allocates nothing should not grow the process.  
Q2 — Is there a better fix than warming up slowly?  
Ten hours at a third of the control resolution is a lot to pay, and it does not  
stop the steps — it only lowers the floor they land on.  
Q3 — What allocates 20–33 MB between two ticks?  
I do not know. What I can say is what it is not: every figure V8 reports  
stays flat across a step, and the libuv handle count does not change. That  
leaves memory the process obtained from the allocator without V8 accounting for  
it.  
If that is glibc arena behaviour, `malloc_trim(0)` would be the remedy, but  
Node does not expose it. `global.gc()` is not a substitute — measured above, it  
changed nothing after a step had happened. Would setting `MALLOC_ARENA_MAX` or  
`MALLOC_TRIM_THRESHOLD_` for app processes be possible, or exposing a trim call  
to apps?  
Q4 — Can the limit be raised for an app that needs it?  
I have read on this forum that `com.athom.matter` is allowed roughly double —  
I have not verified that myself, but if it is right the mechanism exists. My floor is ~100 MB after the workaround, but a single unreturned step  
still takes it to 145 — four MB from the kill.  
Q5 — Can an app read its own RSS?  
Not today. `/proc` is not mounted in the app sandbox; confirmed at runtime:

```auto
process.memoryUsage.rss() -> ENOENT: no such file or directory,
                             uv_resident_set_memory

```

The only figure available in-process is `getrusage(ru_maxrss)`, a high-water  
mark that never decreases and therefore reports every transient peak as if it  
were permanent. I had to write a second app that polls `app.usage.mem` through  
homey-api just to see the real curve — and an app that wants to protect itself  
has to call the REST API to look up its own memory use.  
Exposing current RSS to apps would make this class of problem far easier to  
diagnose, and would let an app back off on its own before it is killed.

### What I have already ruled out

All buffers allocated once at `onInit` and written in place; no per-iteration  
allocation in the poll loop.  
Log strings are never built unless the line is actually emitted.  
Trigger card lookups cached; TCP connections kept alive with keep-alive.  
Chart rendering and all HTTP uploads moved into a separate app: ~92 kB of code  
and 3 MB of buffers left app 1. The ceiling did not change.  
Poll intervals tested at 500, 1000, 1500 and 2000 ms.  
Forced GC tested on and off within the same process.  
libuv handle count monitored continuously — constant at Pipe:1, Socket:4.

diag. showing some manual restarts with different RSS behaviours and one crash.

 ![image](https://us1.discourse-cdn.com/flex025/uploads/athom/original/3X/5/6/56c82d0e849394f3adfa6798ec2d37460d08df8c.png)

---

<div class="post-metadata">

**Author:** ![robertklep](https://sea1.discourse-cdn.com/flex025/user_avatar/community.homey.app/robertklep/32/160628_2.png) [@robertklep](https://community.homey.app/u/robertklep)\
**Post date:** [August 25, 2026, 4:41am UTC](https://community.homey.app/t/rss-grows-in-20-30-mb-steps-between-event-loop-ticks-and-the-first-hours-after-start-decide-the-floor-is-this-known/158723/2 "2026-08-25T04:41:34Z")

</div>

Sorry I didn’t bother to read your essay (AI;DR (“AI, didn’t read”)), but the combination of “polling” and “gradually using more memory” gives off a distinct smell of a memory leak.

Things to try (perhaps you already did):

- decrease the number of capabilities
- decrease the polling interval significantly

Don’t try both things at the same time, start with decreasing the number of capabilities. Provided that the code that’s populating the capabilities is unlikely to leak, it could point to memory leaking from the SDK.

However, given your propensity for using AI for your post, I assume that your apps are also AI-generated, in which case all bets are off. You can _ask_ it to do any of the things that I propose, but you won’t know for certain that it did so in a proper way unless you meticulously validate the code by hand.

---

<div class="post-metadata">

**Author:** ![4x4\_Pete](https://sea1.discourse-cdn.com/flex025/user_avatar/community.homey.app/4x4_pete/32/113286_2.png) [@4x4\_Pete](https://community.homey.app/u/4x4_Pete)\
**Post date:** [August 25, 2026, 8:31am UTC](https://community.homey.app/t/rss-grows-in-20-30-mb-steps-between-event-loop-ticks-and-the-first-hours-after-start-decide-the-floor-is-this-known/158723/3 "2026-08-25T08:31:22Z")

</div>

Maybe better first fix your AI‘s writing style before letting this thing try to fix code. - Or even better: Ask some precise, focused questions, that you have written and understood yourselve.

---

<div class="post-metadata">

**Author:** ![melectro](https://sea1.discourse-cdn.com/flex025/user_avatar/community.homey.app/melectro/32/78999_2.png) [@melectro](https://community.homey.app/u/melectro)\
**Post date:** [August 25, 2026, 10:02am UTC](https://community.homey.app/t/rss-grows-in-20-30-mb-steps-between-event-loop-ticks-and-the-first-hours-after-start-decide-the-floor-is-this-known/158723/4 "2026-08-25T10:02:31Z")

</div>

Fair enough on the length — point taken.

I have two apps on the same Homey. One has 73 capabilities and  
writes about 6300 values an hour. The other has 2 capabilities and writes maybe  
200 an hour. Both take 10–30 MB RSS steps. Doesn’t that rule out capability  
count as the cause?

On the poll interval: I’ve tested 500, 1000, 1500 and 2000 ms. The steps are the  
same size at all four. Going slower than 2000 ms isn’t really an option for me —  
this is load balancing a 10 kW inverter against a 16 A main fuse, so if the loop  
can’t react in a couple of seconds the whole point of the app is gone.

And that’s what I keep getting stuck on. The loop doesn’t allocate anything. It  
reads Modbus registers into buffers that were allocated once at startup. Why  
would running that same loop more often cost more memory at all? That’s the part  
I can’t make sense of, and it’s the reason I asked.

---

<div class="post-metadata">

**Author:** ![robertklep](https://sea1.discourse-cdn.com/flex025/user_avatar/community.homey.app/robertklep/32/160628_2.png) [@robertklep](https://community.homey.app/u/robertklep)\
**Post date:** [August 25, 2026, 11:12am UTC](https://community.homey.app/t/rss-grows-in-20-30-mb-steps-between-event-loop-ticks-and-the-first-hours-after-start-decide-the-floor-is-this-known/158723/5 "2026-08-25T11:12:39Z")

</div>

> [@melectro](#):
>
> Doesn’t that rule out capability count as the cause?

Perhaps. Perhaps not. 🤷🏼‍♂️

> [@melectro](#):
>
> The loop doesn’t allocate anything.  
> …  
> Why would running that same loop more often cost more memory at all?

This feels the same as being a car mechanic and having someone describe that their car is making a strange sound, _without having brought their actual car_.

In other words: without actual code we can guess all we want, but it’s probably a waste of time.

---

<div class="post-metadata">

**Author:** ![Jeroen\_Tebbens](https://sea1.discourse-cdn.com/flex025/user_avatar/community.homey.app/jeroen_tebbens/32/62246_2.png) [@Jeroen\_Tebbens](https://community.homey.app/u/Jeroen_Tebbens)\
**Post date:** [August 25, 2026, 11:29am UTC](https://community.homey.app/t/rss-grows-in-20-30-mb-steps-between-event-loop-ticks-and-the-first-hours-after-start-decide-the-floor-is-this-known/158723/6 "2026-08-25T11:29:03Z")

</div>

Tip: cache the capabilities and make the setCapability() update on a lower frequency and only update upon change:

```auto
async function updateCapability(device, capability, value) {
  try {
    const current = device.getCapabilityValue(capability);

    // --- SAFE REMOVE ---
    // Removal is allowed only when:
    // 1) the new value is null
    // 2) the current value in Homey is also null

    if (value == null && current == null) {
      if (device.hasCapability(capability)) {
        await device.removeCapability(capability);
        device.log(`🗑️ Removed capability "${capability}"`);
      }
      return;
    }

    // --- ADD IF MISSING ---
    if (!device.hasCapability(capability)) {
      try {
        await device.addCapability(capability);
        device.log(`➕ Added capability "${capability}"`);
      } catch (err) {
        if (err && (err.code === 409 || err.statusCode === 409 || (err.message && err.message.includes('capability_already_exists')))) {
          device.log(`Capability already exists: ${capability} — ignoring`);
        } else {
          throw err;
        }
      }
    }

    // --- UPDATE ---
    if (current !== value) {
      await device.setCapabilityValue(capability, value);
    }

  } catch (err) {
    if (err.message === 'device_not_found') {
      device.log(`⚠️ Skipping capability "${capability}" — device not found`);
      return;
    }
    device.error(`❌ Failed updateCapability("${capability}")`, err);
  }
}

```

---

<div class="post-metadata">

**Author:** ![robertklep](https://sea1.discourse-cdn.com/flex025/user_avatar/community.homey.app/robertklep/32/160628_2.png) [@robertklep](https://community.homey.app/u/robertklep)\
**Post date:** [August 25, 2026, 11:39am UTC](https://community.homey.app/t/rss-grows-in-20-30-mb-steps-between-event-loop-ticks-and-the-first-hours-after-start-decide-the-floor-is-this-known/158723/7 "2026-08-25T11:39:16Z")

</div>

> [@Jeroen\_Tebbens](#):
>
> Tip: cache the capabilities and make the setCapability() update on a lower frequency and only update upon change:

That’s another thing to try: don’t update _any_ capabilities in the polling loop, just retrieve the device data.

---

<div class="post-metadata">

**Author:** ![melectro](https://sea1.discourse-cdn.com/flex025/user_avatar/community.homey.app/melectro/32/78999_2.png) [@melectro](https://community.homey.app/u/melectro)\
**Post date:** [August 25, 2026, 4:59pm UTC](https://community.homey.app/t/rss-grows-in-20-30-mb-steps-between-event-loop-ticks-and-the-first-hours-after-start-decide-the-floor-is-this-known/158723/8 "2026-08-25T16:59:05Z")

</div>

@Jeroen_Tebbens — thanks. That is already how it works here; every capability write goes through one function that skips unchanged values:

js

```auto
async _capSetM(cap, v) {
  if (!this._pollDoDisplay) return true; // display throttle
  if (this.getCapabilityValue(cap) === v) return true; // skip if unchanged
  ...
}

```

The 6300 writes/hour I quoted is what is left after that filter.

@robertklep — I ran your test. Result below.

**Setup.** I replaced `setCapabilityValue` on the device instance so every call is counted and discarded. Not a flag inside my own setter — there are 77 call sites in the file and only two go through it, so a flag there would have missed 75 of them. Modbus reads and the control logic were untouched. Poll interval 1500 ms, the same as the run I am comparing against.

**Writes suppressed: 69 301 in 87 minutes** (≈810/min), logged every 60 s so that “zero writes reached the device layer” is measured, not assumed.

**Result: no difference.**

```auto
                                    growth outcome
previous run, writes enabled +3.7 MB/h -
this run, writes disabled +5.5 MB/h +33.9 MB permanent step at 87 min

```

At the moment of the step my page-fault instrumentation says:

```auto
[RSSATTR] between polls: 6290 minor faults = 24.6 MB (gap 1474 ms)
[MEMJUMP] fsm=DAY_GRID_IDLE op=idle heapUsed 21 heapTotal 24.9 external 4
          malloced 0.5 handles 5 state unchanged for 1637 s

```

24.6 MB arrived between two polls, across a 1474 ms gap, with the state machine idle for 27 minutes, three major GCs in the preceding 75 seconds, two Modbus writes in the last minute, and not one capability write in the entire process lifetime. Every V8 figure flat, handle count unchanged.

 ![image](https://us1.discourse-cdn.com/flex025/uploads/athom/original/3X/5/0/50c05f25c25cf003e0fc0b1d885c522a5be81de8.png)

So capability writes are not the cause. I am glad to have it ruled out rather than assumed — thank you for the suggestion.

That leaves the same question as before: what obtains 20–33 MB from the allocator between two event-loop iterations, while V8 accounts for none of it.

On posting the code: the device file is around 19 000 lines, and I do not think anyone wants to read that — which is the main reason it has not been uploaded. The poll loop and the memory instrumentation are the parts this question is actually about, and I am happy to post those on their own if that is useful.

---

<div class="post-metadata">

**Author:** ![melectro](https://sea1.discourse-cdn.com/flex025/user_avatar/community.homey.app/melectro/32/78999_2.png) [@melectro](https://community.homey.app/u/melectro)\
**Post date:** [August 25, 2026, 7:17pm UTC](https://community.homey.app/t/rss-grows-in-20-30-mb-steps-between-event-loop-ticks-and-the-first-hours-after-start-decide-the-floor-is-this-known/158723/9 "2026-08-25T19:17:44Z")

</div>

After the test where no capability update was disabled, I turned the update on and the app ran about 2.5h into a crash.

 ![image](https://us1.discourse-cdn.com/flex025/uploads/athom/original/3X/5/7/57a72a4b90266159ff7275ff7ac97797f514c94c.png)

---

<div class="post-metadata">

**Author:** ![robertklep](https://sea1.discourse-cdn.com/flex025/user_avatar/community.homey.app/robertklep/32/160628_2.png) [@robertklep](https://community.homey.app/u/robertklep)\
**Post date:** [August 26, 2026, 4:31am UTC](https://community.homey.app/t/rss-grows-in-20-30-mb-steps-between-event-loop-ticks-and-the-first-hours-after-start-decide-the-floor-is-this-known/158723/10 "2026-08-26T04:31:36Z")

</div>

> [@melectro](#):
>
> So capability writes are not the cause.

Step two: no Modbus interactions in the polling loop at all.

---

<div class="post-metadata">

**Author:** ![melectro](https://sea1.discourse-cdn.com/flex025/user_avatar/community.homey.app/melectro/32/78999_2.png) [@melectro](https://community.homey.app/u/melectro)\
**Post date:** [August 26, 2026, 5:09am UTC](https://community.homey.app/t/rss-grows-in-20-30-mb-steps-between-event-loop-ticks-and-the-first-hours-after-start-decide-the-floor-is-this-known/158723/11 "2026-08-26T05:09:14Z")

</div>

This is a test I carried out shortly after I first discovered the crashes (2 months ago). Modbus has no impact on this RSS issue. It was at that point that I realized that Homey does not shut down the system properly.

I have now also contacted Athom regarding that issue and reminded them that there is a four-year-old case that still remains unresolved:

> <https://github.com/athombv/homey-apps-sdk-issues/issues/451>
>
> Hello,
> 
> I have noticed what appears to be an issue with the Homey SDK3 applicati…on lifecycle and would like to ask whether this is expected behavior or a bug.
> 
> I have implemented onUninit() in my app.js:
> 
> async onUninit() {
> this.log('\[APP\]\[UNINIT\] App#onUninit called');
> }
> 
> The purpose is to gracefully close my Modbus TCP connection before the application exits.
> 
> However, onUninit() is never called.
> 
> I have tested the following scenarios:
> 
> Restarting the app from the Homey Apps settings page.
> Stopping the development session with Ctrl+C while running homey app run.
> 
> In both cases, the application exits without ever logging \[APP\]\[UNINIT\].
> 
> To verify that the Node.js process actually reaches shutdown, I also added:
> 
> process.on('exit', code =\> {
> console.log('\[PROCESS\] exit', code);
> });
> 
> This callback is executed every time, for example:
> 
> \[PROCESS\] exit 1
> 
> The application then starts again normally and onInit() is called, but onUninit() is never executed.
> 
> According to the SDK3 documentation, App.onUninit() should be called when the app is destroyed, so I expected this callback to be invoked before the process exits.
> 
> Could you please clarify:
> 
> Is App.onUninit() expected to be called when an app is restarted from Homey's Apps settings?
> Is it expected to be called when a development app is stopped with homey app run (Ctrl+C)?
> If not, what is the recommended way to perform asynchronous cleanup (for example, gracefully closing TCP sockets or terminating worker threads) before the application exits?
> 
> This behavior is important for my application because it communicates with a Sungrow inverter via Modbus TCP. If the connection is not closed gracefully, the inverter may temporarily refuse new Modbus connections after the app restarts.
> 
> Additional information:
> 
> I also tested the documented Homey unload event:
> 
> this.homey.on('unload', () =\> {
> this.log('\[APP\] unload');
> });
> 
> This callback is never invoked either, regardless of whether the app is restarted from Homey's Apps settings or stopped using homey app run.
> 
> The only callback that is consistently executed is:
> 
> process.on('exit', code =\> {
> console.log('\[PROCESS\] exit', code);
> });
> 
> which indicates that the Node.js process exits without App.onUninit() or the documented Homey unload event being triggered.
> Thank you for your help.

---

<div class="post-metadata">

**Author:** ![Jeroen\_Tebbens](https://sea1.discourse-cdn.com/flex025/user_avatar/community.homey.app/jeroen_tebbens/32/62246_2.png) [@Jeroen\_Tebbens](https://community.homey.app/u/Jeroen_Tebbens)\
**Post date:** [August 26, 2026, 5:22am UTC](https://community.homey.app/t/rss-grows-in-20-30-mb-steps-between-event-loop-ticks-and-the-first-hours-after-start-decide-the-floor-is-this-known/158723/12 "2026-08-26T05:22:53Z")

</div>

No code base → still guessing

- Http agent - keep alive on fetch? Vs connect / close every 1500ms
- free/ max sockets? (Also http agent parameter)
- Shared fs storage place vs this.homey.settings.set(key, value) ?

---

<div class="post-metadata">

**Author:** ![melectro](https://sea1.discourse-cdn.com/flex025/user_avatar/community.homey.app/melectro/32/78999_2.png) [@melectro](https://community.homey.app/u/melectro)\
**Post date:** [August 26, 2026, 5:32am UTC](https://community.homey.app/t/rss-grows-in-20-30-mb-steps-between-event-loop-ticks-and-the-first-hours-after-start-decide-the-floor-is-this-known/158723/13 "2026-08-26T05:32:43Z")

</div>

OK, now that I have given you advance notice that this is a large file (device.js), it is available for download via SwissTransfer at the following link: [SwissTransfer - Send large files securely and free of charge](https://www.swisstransfer.com/dl/01a03c8b-c010-7000-b300-b79fd5c066d9)

---

<div class="post-metadata">

**Author:** ![robertklep](https://sea1.discourse-cdn.com/flex025/user_avatar/community.homey.app/robertklep/32/160628_2.png) [@robertklep](https://community.homey.app/u/robertklep)\
**Post date:** [August 26, 2026, 6:18am UTC](https://community.homey.app/t/rss-grows-in-20-30-mb-steps-between-event-loop-ticks-and-the-first-hours-after-start-decide-the-floor-is-this-known/158723/14 "2026-08-26T06:18:02Z")

</div>

There is _so_ much stuff layered on top of other stuff (layered on top of _other_ stuff) that this is simply undebuggable. There could probably be a few dozen reasons why this is not working properly, and none have to do with either Node.js or Homey.

---

<div class="post-metadata">

**Author:** ![melectro](https://sea1.discourse-cdn.com/flex025/user_avatar/community.homey.app/melectro/32/78999_2.png) [@melectro](https://community.homey.app/u/melectro)\
**Post date:** [August 26, 2026, 7:30am UTC](https://community.homey.app/t/rss-grows-in-20-30-mb-steps-between-event-loop-ticks-and-the-first-hours-after-start-decide-the-floor-is-this-known/158723/15 "2026-08-26T07:30:50Z")

</div>

To clarify the current situation, the code itself is functioning exactly as intended with regard to its primary purpose and functionality. The application is performing the tasks it is designed to perform correctly.

The issue I am investigating is the seemingly **“random”** increases in RSS memory usage. These increases are not expected or acceptable, and they eventually cause Homey to terminate the app and restart it.

I can only hope that the issue with getting Homey to actually execute the code in `onUninit()` will be resolved. If that works as intended, I will be able to close the Modbus connection properly, preventing the communication from becoming stuck in the inverter. At present, this causes the inverter to remain unresponsive when the app is restarted under a different process ID.

I will have to wait for Athom’s response. Their support has told me that a developer has been assigned to investigate the problem, but it has now been almost a month without any response.

---

<div class="post-metadata">

**Author:** ![robertklep](https://sea1.discourse-cdn.com/flex025/user_avatar/community.homey.app/robertklep/32/160628_2.png) [@robertklep](https://community.homey.app/u/robertklep)\
**Post date:** [August 26, 2026, 7:41am UTC](https://community.homey.app/t/rss-grows-in-20-30-mb-steps-between-event-loop-ticks-and-the-first-hours-after-start-decide-the-floor-is-this-known/158723/16 "2026-08-26T07:41:49Z")

</div>

> [@melectro](#):
>
> To clarify the current situation, the code itself is functioning exactly as intended with regard to its primary purpose and functionality. The application is performing the tasks it is designed to perform correctly.

Spaghetti code usually does 🤷🏼‍♂️

> [@melectro](#):
>
> The issue I am investigating is the seemingly **“random”** increases in RSS memory usage. These increases are not expected or acceptable.

I disagree. There is _so_ much going on in the code that it doesn’t surprise me at all.

> [@melectro](#):
>
> I can only hope that the issue with getting Homey to actually execute the code in `onUninit()` will be resolved.

Although I agree Athom should fix that issue before we celebrate its fourth birthday in a couple of weeks, when it comes to your app it’s just helpful to manage symptoms, not to fix the actual issue.

---

<div class="post-metadata">

**Author:** ![Jeroen\_Tebbens](https://sea1.discourse-cdn.com/flex025/user_avatar/community.homey.app/jeroen_tebbens/32/62246_2.png) [@Jeroen\_Tebbens](https://community.homey.app/u/Jeroen_Tebbens)\
**Post date:** [August 26, 2026, 7:43am UTC](https://community.homey.app/t/rss-grows-in-20-30-mb-steps-between-event-loop-ticks-and-the-first-hours-after-start-decide-the-floor-is-this-known/158723/17 "2026-08-26T07:43:09Z")

</div>

If it was raised 4 years ago, and now related again pending 1 month I would suggest not hold your breath on this.

Further looking at the code try to understand the size of the “blobs” you are writing via this.homey.settings.set() and read this.homey.settings.get().

These blobs can easily explode and eat exactly your memory (RSS).  
I’ve come across this myself in my own app.

And please start splitting functions in to separate references (files) to make it readable.

Fetches on:

- Weather
- Prices
- Your own url
- Make toISOString() get it once as this is a system call (cpu hog)

---

<div class="post-metadata">

**Author:** ![melectro](https://sea1.discourse-cdn.com/flex025/user_avatar/community.homey.app/melectro/32/78999_2.png) [@melectro](https://community.homey.app/u/melectro)\
**Post date:** [August 26, 2026, 11:05am UTC](https://community.homey.app/t/rss-grows-in-20-30-mb-steps-between-event-loop-ticks-and-the-first-hours-after-start-decide-the-floor-is-this-known/158723/18 "2026-08-26T11:05:04Z")

</div>

> [@robertklep](#):
>
> Athom should fix that issue before we celebrate its fourth birthday in a couple of weeks

If the communication does not get stuck, then once `onUninit` works as described in SDK3 and my code is able to start up properly, this is something I can live with. A restart only takes a few seconds, and if it happens on average once a day, it is merely an annoyance rather than a major problem.

Since I am, probably, the only one who will be using the app, I do not need to take the same considerations into account as I would if the app were published. In that case, one would expect the code not to be spaghetti code, and basic restart-related bugs simply cannot be present.

---

<div class="post-metadata">

**Author:** ![robertklep](https://sea1.discourse-cdn.com/flex025/user_avatar/community.homey.app/robertklep/32/160628_2.png) [@robertklep](https://community.homey.app/u/robertklep)\
**Post date:** [August 26, 2026, 11:28am UTC](https://community.homey.app/t/rss-grows-in-20-30-mb-steps-between-event-loop-ticks-and-the-first-hours-after-start-decide-the-floor-is-this-known/158723/19 "2026-08-26T11:28:06Z")

</div>

In your bug report you state that triggering on the `exit` event _does_ work. Is there perhaps an option to close the Modbus connection in a synchronous way (since you can _only_ use synchronous operations in an `exit` handler)?

---

<div class="post-metadata">

**Author:** ![melectro](https://sea1.discourse-cdn.com/flex025/user_avatar/community.homey.app/melectro/32/78999_2.png) [@melectro](https://community.homey.app/u/melectro)\
**Post date:** [August 26, 2026, 12:14pm UTC](https://community.homey.app/t/rss-grows-in-20-30-mb-steps-between-event-loop-ticks-and-the-first-hours-after-start-decide-the-floor-is-this-known/158723/20 "2026-08-26T12:14:10Z")

</div>

When, for example, I press **Ctrl+C** to stop the running application, `onUninit()` is not executed. I see this happening repeatedly, and when the application is started again, the Modbus connection does not come back up.

I have therefore implemented a workaround in my code: I have added a boolean button to the device’s detail view. When pressed, it disconnects the Modbus connection, thereby releasing the connection between Homey and the inverter, before I press CTRL+C or restart.

**Is this the functionality you have identified?**

[Next page](https://community.homey.app/t/rss-grows-in-20-30-mb-steps-between-event-loop-ticks-and-the-first-hours-after-start-decide-the-floor-is-this-known/158723.md?page=2)
