Version
2026.07.02
Unraid Version
7.2.6
Bug Description
First off wanted to apologize for the copy and pasted Claude responses but after digging into this issue I thought it best as it seemed to paint the clearest picture of what was going on and what steps I had taken and found to be a problem. Thanks so much and happy to provide any additional information, logs etc that would be helpful. Cheers!
In the default live stats rate mode, plugins/compose.manager/nchan/compose_info publishes the entire serialized stats payload once per docker stats line, rather than once per refresh cycle. Since the payload contains every container, publish volume is O(n²) in container count.
On my host (146 running containers, only 2 of which are compose-managed) this generates ~4.7 MB/sec of nchan traffic and exhausts nginx's 256 MiB nchan shared memory pool in about 70 seconds.
nchan is shared across the entire Unraid webGUI, so this breaks unrelated functionality. Most visibly, the Docker container update progress modal hangs on "In Progress" forever with no streaming output and no Done button (the update itself completes fine — only the progress stream is dead).
Root cause
Three things compound:
1. docker stats is unfiltered — it streams every running container on the host, not just compose-managed ones:
$cmd = "docker stats --format='{{.ID}};{{.CPUPerc}};{{.MemUsage}};{{.MemPerc}};{{.NetIO}};{{.BlockIO}}' 2>&1";
2. live mode disables the publish gate, and is also the default when unset:
$mode = strtolower(trim((string)($cfg['COMPOSE_STATS_RATE_MODE'] ?? 'live')));
resolvePublishIntervalMs() returns 0 for live.
3. The gate is therefore unconditionally true, once per line:
if ($publishIntervalMs === 0 || ($nowMs - $lastPublishMs) >= $publishIntervalMs) {
$payload = buildPayload($statsById); // serializes ALL rows
publish('composeinfo', $payload, 1, true, 10);
}
Each incoming line updates exactly one entry in $statsById, but buildPayload() re-serializes the whole map. One full refresh of N containers = N publishes of an N-row payload.
Measured
From /tmp/compose_info_nchan.status at the moment of failure:
{"state":"running","ts":1784120109,"mode":"live","intervalMs":0,"rows":146,
"updates":25740,"cadenceAvgMs":2,"cadenceMinMs":1,"cadenceMaxMs":502,"cadenceLastMs":2}
rows: 146 matches docker ps -q | wc -l exactly — confirms it publishes all containers, not the 2 compose-managed ones
cadenceAvgMs: 2 → ~430 stats lines/sec → ~430 publishes/sec
- Payload ~10,974 bytes (per the nginx error) → ~4.7 MB/sec into nchan
The arithmetic closes almost exactly on exhaustion:
25,740 publishes × 10,974 bytes ≈ 269 MB
nchan pool = 262,144 KiB = 268 MB
Cumulative published bytes hit the pool ceiling — nchan's reaper can't reclaim anywhere near fast enough at this rate. Process started 08:54, status ts decodes to 08:55:09, exhaustion logged 08:55:10.
Expected Behavior
The publisher should emit at most one message per refresh cycle (or per configured interval), not one per docker stats line. Publish volume should scale linearly with container count, not quadratically.
Additionally:
docker stats should be filtered to compose-managed container IDs. It accepts explicit IDs — this would cut my payload from 146 rows to 2 and eliminates the quadratic term at the source.
- The default rate mode should not be
live. It isn't safe on hosts with many containers.
- The publisher should recover after nchan returns errors, rather than wedging permanently.
Steps to Reproduce
- Unraid host with a large number of running containers (effect scales quadratically; ~100+ makes it fast — I have 146)
- Leave
COMPOSE_STATS_RATE_MODE unset, so it defaults to live
- Open the Compose tab to spawn the
compose_info publisher
- Watch
/var/log/nginx/error.log — ngx_slab_alloc() failed: no memory appears within ~1–2 minutes
- Confirm the rate:
cat /tmp/compose_info_nchan.status — note intervalMs: 0, cadenceAvgMs: 2, and rows equal to your full docker ps -q | wc -l
- Try updating any container from the Docker tab — the progress modal hangs on "In Progress" with no output and no Done button
Note: only 2 of my 146 containers are compose-managed, so the payload size is unrelated to how much I actually use Compose.
Relevant Logs
2026/07/15 08:55:10 [crit] 3836496#3836496: ngx_slab_alloc() failed: no memory
2026/07/15 08:55:10 [error] 3836496#3836496: shpool alloc failed
2026/07/15 08:55:10 [error] 3836496#3836496: nchan: Out of shared memory while allocating message of size 10974. Increase nchan_max_reserved_memory.
2026/07/15 08:55:10 [error] 3836496#3836496: *91477 nchan: error publishing message (HTTP status code 500), client: unix:, server: , request: "POST /pub/composeinfo?buffer_length=1 HTTP/1.1", host: "localhost"
2026/07/15 08:55:10 [error] 3836496#3836496: MEMSTORE:01: can't create shared message for channel /composeinfo
2026/07/15 08:55:10 [info] 2028771#2028771: Using 116KiB of shared memory for nchan in /etc/nginx/nginx.conf:162
2026/07/15 08:55:10 [info] 2028771#2028771: Using 262144KiB of shared memory for nchan in /etc/nginx/nginx.conf:162
syslog:
Jul 10 20:15:17 CMacServer compose.manager: [INFO] [nchan] publisher starting
Jul 10 20:15:17 CMacServer compose.manager: [INFO] [nchan] starting docker stats stream (mode=live, intervalMs=0)
status file at time of failure:
{"state":"running","ts":1784120109,"mode":"live","intervalMs":0,"rows":146,"updates":25740,"cadenceAvgMs":2,"cadenceMinMs":1,"cadenceMaxMs":502,"cadenceLastMs":2}
Compose
Not relevant to this bug — the payload contains all 146 running containers regardless of compose usage. I have only 2 compose stacks (a redis event-watcher sidecar and a redlib instance with a watchdog), neither of which influences the publisher's behavior.
Additional Context
Secondary issue 1: publisher wedges after exhaustion
Once nchan starts returning 500s, the process stays alive but its loop stalls — /tmp/compose_info_nchan.status froze at 08:55:09 and was still stale 4 hours later. It never recovers on its own, even after Unraid's watchdog auto-reloads nginx at 08:55:10.
Secondary issue 2: the idle branch ignores the configured interval
Because the stream is non-blocking, fgets() returns false frequently between lines, and this fires regardless of $publishIntervalMs:
} else {
if (($nowMs - $lastListenerCheckMs) >= 1000) {
$payload = buildPayload($statsById);
publish('composeinfo', $payload, 1, true, 10);
$lastListenerCheckMs = $nowMs;
}
usleep(100000);
}
This puts a hard floor of ~1 publish/sec on the channel even at custom/10000ms, so intervals above 1s can't actually be honored.
Suggested fixes
- Coalesce publishes in
live mode — apply a small floor (e.g. 250ms) and publish the accumulated map rather than treating intervalMs === 0 as "publish every line"
- Change the default from
live to slow/medium
- Filter
docker stats to compose-managed container IDs
- Make the idle keepalive respect the configured interval instead of hardcoding 1000ms
- Consider publishing deltas rather than the full map each tick
Version
2026.07.02
Unraid Version
7.2.6
Bug Description
First off wanted to apologize for the copy and pasted Claude responses but after digging into this issue I thought it best as it seemed to paint the clearest picture of what was going on and what steps I had taken and found to be a problem. Thanks so much and happy to provide any additional information, logs etc that would be helpful. Cheers!
In the default
livestats rate mode,plugins/compose.manager/nchan/compose_infopublishes the entire serialized stats payload once perdocker statsline, rather than once per refresh cycle. Since the payload contains every container, publish volume is O(n²) in container count.On my host (146 running containers, only 2 of which are compose-managed) this generates ~4.7 MB/sec of nchan traffic and exhausts nginx's 256 MiB nchan shared memory pool in about 70 seconds.
nchan is shared across the entire Unraid webGUI, so this breaks unrelated functionality. Most visibly, the Docker container update progress modal hangs on "In Progress" forever with no streaming output and no Done button (the update itself completes fine — only the progress stream is dead).
Root cause
Three things compound:
1.
docker statsis unfiltered — it streams every running container on the host, not just compose-managed ones:2.
livemode disables the publish gate, and is also the default when unset:resolvePublishIntervalMs()returns0forlive.3. The gate is therefore unconditionally true, once per line:
Each incoming line updates exactly one entry in
$statsById, butbuildPayload()re-serializes the whole map. One full refresh of N containers = N publishes of an N-row payload.Measured
From
/tmp/compose_info_nchan.statusat the moment of failure:{"state":"running","ts":1784120109,"mode":"live","intervalMs":0,"rows":146, "updates":25740,"cadenceAvgMs":2,"cadenceMinMs":1,"cadenceMaxMs":502,"cadenceLastMs":2}rows: 146matchesdocker ps -q | wc -lexactly — confirms it publishes all containers, not the 2 compose-managed onescadenceAvgMs: 2→ ~430 stats lines/sec → ~430 publishes/secThe arithmetic closes almost exactly on exhaustion:
Cumulative published bytes hit the pool ceiling — nchan's reaper can't reclaim anywhere near fast enough at this rate. Process started 08:54, status
tsdecodes to 08:55:09, exhaustion logged 08:55:10.Expected Behavior
The publisher should emit at most one message per refresh cycle (or per configured interval), not one per
docker statsline. Publish volume should scale linearly with container count, not quadratically.Additionally:
docker statsshould be filtered to compose-managed container IDs. It accepts explicit IDs — this would cut my payload from 146 rows to 2 and eliminates the quadratic term at the source.live. It isn't safe on hosts with many containers.Steps to Reproduce
COMPOSE_STATS_RATE_MODEunset, so it defaults tolivecompose_infopublisher/var/log/nginx/error.log—ngx_slab_alloc() failed: no memoryappears within ~1–2 minutescat /tmp/compose_info_nchan.status— noteintervalMs: 0,cadenceAvgMs: 2, androwsequal to your fulldocker ps -q | wc -lNote: only 2 of my 146 containers are compose-managed, so the payload size is unrelated to how much I actually use Compose.
Relevant Logs
Compose
Not relevant to this bug — the payload contains all 146 running containers regardless of compose usage. I have only 2 compose stacks (a redis event-watcher sidecar and a redlib instance with a watchdog), neither of which influences the publisher's behavior.Additional Context
Secondary issue 1: publisher wedges after exhaustion
Once nchan starts returning 500s, the process stays alive but its loop stalls —
/tmp/compose_info_nchan.statusfroze at 08:55:09 and was still stale 4 hours later. It never recovers on its own, even after Unraid's watchdog auto-reloads nginx at 08:55:10.Secondary issue 2: the idle branch ignores the configured interval
Because the stream is non-blocking,
fgets()returnsfalsefrequently between lines, and this fires regardless of$publishIntervalMs:This puts a hard floor of ~1 publish/sec on the channel even at
custom/10000ms, so intervals above 1s can't actually be honored.Suggested fixes
livemode — apply a small floor (e.g. 250ms) and publish the accumulated map rather than treatingintervalMs === 0as "publish every line"livetoslow/mediumdocker statsto compose-managed container IDs