diff --git a/.env.example b/.env.example
index 8c35898486..b59476ffcd 100644
--- a/.env.example
+++ b/.env.example
@@ -298,11 +298,21 @@ REDIS_URL=redis://redis:6379 # REQUIRED for the self-host review
# # a moderate-load instance (several active repos / a steady
# # contributor-PR stream) on a host with a few spare cores -- review
# # jobs are I/O-bound (GitHub + AI awaits dominate), so raising this
-# # mostly buys parallelism, not CPU. Watch gittensory_queue_live_pending
+# # mostly buys parallelism, not CPU. As a core-count anchor (#1828):
+# # a 2-vCPU host is comfortable around the default of 4; a 4+ vCPU
+# # host can reasonably go to 8-12 before the loops' own CPU work
+# # (JSON parsing, diff rendering, AI-response handling) starts
+# # competing for the cycles the I/O awaits were supposed to free up.
+# # Watch gittensory_queue_live_pending
# # / gittensory_queue_oldest_live_pending_age_seconds after raising it;
# # if those stay high, the bottleneck is elsewhere (GitHub rate limit,
# # AI latency, Postgres pool -- see PGPOOL_MAX above), not concurrency.
# QUEUE_BACKGROUND_CONCURRENCY=1 # max low-priority/background jobs allowed to occupy QUEUE_CONCURRENCY slots
+# # (default 1 reserves the rest of even a small 2-vCPU host's default
+# # 4 slots for live review work). Scale it WITH QUEUE_CONCURRENCY, not
+# # independently -- keep it to roughly a quarter to a third of
+# # QUEUE_CONCURRENCY (e.g. 2-3 of a 4+ vCPU host's 8-12) so background
+# # maintenance can never crowd out every live-review slot.
# CONTRIBUTOR_EVIDENCE_BATCH_SIZE=150 # logins per build-contributor-evidence job; the scheduled run fans out into
# # per-batch jobs above this so the per-login GitHub reads spread across the
# # queue instead of bursting. Set 0 to disable the fan-out (single job).
diff --git a/apps/gittensory-ui/src/routes/docs.self-hosting-operations.tsx b/apps/gittensory-ui/src/routes/docs.self-hosting-operations.tsx
index e5dc7c6248..0b1edb6517 100644
--- a/apps/gittensory-ui/src/routes/docs.self-hosting-operations.tsx
+++ b/apps/gittensory-ui/src/routes/docs.self-hosting-operations.tsx
@@ -328,16 +328,20 @@ DISCORD_REPO_WEBHOOKS={"owner/repoA":"https://discord.com/api/webhooks/...","own
+ --profile runners
RUNNER_MEM_LIMIT (default 2g) per replica
runner service ships with no CPU/memory limit at all. Production
- experience already documented in docker-compose.override.yml.example{" "}
- found 3 uncapped runner containers starving the app for CPU on an 8-vCPU box under
- real CI load — see that file for the cpu_shares/cpus{" "}
- mitigation before co-locating runners with the review stack.
+ Estimated, and explicitly a known risk on the CPU side, not a guess about typical
+ usage: the runner service ships with a default memory ceiling (
+ RUNNER_MEM_LIMIT, default 2g, added by #3893) but no CPU limit.
+ Production experience already documented in{" "}
+ docker-compose.override.yml.example found 3 uncapped runner containers
+ starving the app for CPU on an 8-vCPU box under real CI load — see that file for the{" "}
+ cpu_shares/cpus mitigation before co-locating runners with
+ the review stack.
--profile runners, which ships with no limit at all; that is a known,
- documented tradeoff (see the table above and{" "}
+ --profile runners's CPU side: the service has a default memory ceiling (
+ RUNNER_MEM_LIMIT, default 2g) but ships with no CPU limit at all; that is a
+ known, documented tradeoff (see the table above and{" "}
docker-compose.override.yml.example) rather than an oversight, since the right
- ceiling depends entirely on the host's core count and how many runner replicas you run.
+ CPU ceiling depends entirely on the host's core count and how many runner replicas you run.