From e6aeaa0a8b0ae21a38279680816ef3748634a0d3 Mon Sep 17 00:00:00 2001 From: fabi Date: Wed, 12 Aug 2026 23:11:57 +0200 Subject: [PATCH] docs(compose): the app CPU cap cannot separate compression from the request path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The comment read "CPU ceiling for the two image workers + ffmpeg poster extraction", which describes a separation Docker cannot make: `compression.rs` runs that work in `tokio::task::spawn_blocking` — same process, same cgroup as every Axum handler — and `cpus`/`cpu_shares` are per-container. What actually happens is worth knowing when sizing this box: `cpu.max` is `120000 100000`, so two CPU-pegged blocking workers exhaust the 120 ms quota after ~60 ms of each 100 ms period and the kernel freezes the WHOLE cgroup — uploads, feed and SSE included — for the rest of it. Over a 100-photo burst that is ~210 s during which every request can eat up to 40 ms of throttle. The value stays at 1.2: an app that can take both cores starves Postgres, and every request path goes through Postgres. A slightly stalled request beats a starved database. The comment now says which knob actually shortens the backlog (COMPRESSION_WORKER_CONCURRENCY) rather than implying this one does. --- docker-compose.yml | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index e41a8b6..17fc59e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -178,9 +178,22 @@ services: # Bounds a runaway ffmpeg transcode (large uploads, 2 workers) so it can't # OOM the single box and take down Postgres. memory: 1G - # CPU ceiling for the two image workers + ffmpeg poster extraction. Bounded below - # 2.0 so the app can never take both cores on its own. + # CPU ceiling for the WHOLE app container. Bounded below 2.0 so it can never take both + # cores on its own, which is what protects Postgres. # COMPRESSION_WORKER_CONCURRENCY=2 is the memory bound; this is the CPU one. + # + # It does NOT cap "the two image workers + ffmpeg" separately from the request path, as + # this used to claim. `compression.rs` runs that work in `tokio::task::spawn_blocking` — + # same process, same cgroup as every Axum handler — and `cpus`/`cpu_shares` are + # per-container, so nothing here can tell them apart. Concretely: `cpu.max` is + # `120000 100000`, so two CPU-pegged blocking workers exhaust the 120 ms quota after + # ~60 ms of each 100 ms period and the kernel then freezes the ENTIRE cgroup — uploads, + # feed and SSE included — for the remainder. Across a 100-photo burst (~210 s of + # draining) every request in that window can eat up to 40 ms of throttle stall. + # + # Kept anyway: an app that can take both cores starves Postgres, and every request path + # goes through Postgres. A slightly stalled request beats a starved database. If the + # backlog needs to drain faster, the knob is COMPRESSION_WORKER_CONCURRENCY, not this. cpus: '1.2' # Half the default weight, and this is the ceiling's other half: the cap alone leaves # 0.8 vCPU for db + frontend + caddy, which frontend and caddy can consume between them.