Skip to content

[Bug]: compose_info publishes full payload per docker-stats line in live mode — exhausts nginx nchan shared memory in ~70s #130

Description

@Healzangels

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

  1. Unraid host with a large number of running containers (effect scales quadratically; ~100+ makes it fast — I have 146)
  2. Leave COMPOSE_STATS_RATE_MODE unset, so it defaults to live
  3. Open the Compose tab to spawn the compose_info publisher
  4. Watch /var/log/nginx/error.logngx_slab_alloc() failed: no memory appears within ~1–2 minutes
  5. 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
  6. 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

  1. 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"
  2. Change the default from live to slow/medium
  3. Filter docker stats to compose-managed container IDs
  4. Make the idle keepalive respect the configured interval instead of hardcoding 1000ms
  5. Consider publishing deltas rather than the full map each tick

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions