Skip to content

fix(server): default to 4 API workers and fix the WEB_SERVER_CONCURRENCY guidance - #4385

Merged
gantoine merged 3 commits into
rommapp:masterfrom
Spinnich:docs/web-server-concurrency-recommendation
Sep 7, 2026
Merged

fix(server): default to 4 API workers and fix the WEB_SERVER_CONCURRENCY guidance#4385
gantoine merged 3 commits into
rommapp:masterfrom
Spinnich:docs/web-server-concurrency-recommendation

Conversation

@Spinnich

@Spinnich Spinnich commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Description

RomM ships one API worker by default, and documents raising it with a heuristic that makes things worse. This PR fixes both: it moves the default to 4 and replaces the recommendation with one that matches how RomM actually serves requests.

Why 2 x CPU cores + 1 is the wrong rule

It is gunicorn's guidance for sync workers, where each worker serves exactly one request at a time and you over-provision to hide blocking I/O. RomM runs --worker-class uvicorn_worker.UvicornWorker with --worker-connections 1000 (docker/init_scripts/init:140-146), and its endpoints are plain def (e.g. get_roms in backend/endpoints/roms/__init__.py), so Starlette already serves them concurrently in its threadpool inside a single worker. The rule does not transfer, and following it costs both latency and memory.

The old note also said the setting covers "API calls and tasks". It does not. WEB_SERVER_CONCURRENCY appears exactly once in the entrypoint, on the gunicorn API server (init:141). Background tasks run in the separate RQ worker processes started by start_bin_rq_worker / start_bin_rq_scan_worker.

Measurements

Real gunicorn, 14 concurrent gallery requests, RomM 5.2.0, 4,644 ROMs over 4 platforms, 16-core / 7.4 GB host (so the documented value works out to 33). Slowest TTFB in the burst:

Workers Slowest TTFB Peak DB connections Errors
1 (old default) 6.474 s 5 0
2 3.515 s 8 0
4 (new default) 1.939 s 14 0
8 2.007 s 14 0
16 1.784 s 16 0
33 (2 x cores + 1) 5.767 s 17 0

Returns flatten after about 4 workers. At 33, processes contend for 16 cores on CPU-bound work and lose more than they gain: 3x slower than 4 workers, and barely better than a single worker.

Memory is the harder limit at the top end. 33 workers measured 5.31 GB PSS (~160 MB per worker, shared pages counted once), leaving 587 MB free on a 7.4 GB host. On an 8-core NAS the old recommendation gives 17 workers, roughly 2.7 GB, competing with every other container. Four workers costs about 640 MB.

Changes

  • docker/init_scripts/init - ${WEB_SERVER_CONCURRENCY:-1} becomes ${WEB_SERVER_CONCURRENCY:-4}
  • env.template - WEB_SERVER_CONCURRENCY=4 # Number of API worker processes, not background tasks; ~160MB RAM each, raise only for several concurrent users
  • examples/docker-compose.example.yml - WEB_SERVER_CONCURRENCY=4 # Workers for API calls, not background tasks. ~160MB RAM each, raise only for several concurrent users

All three now agree. Before this PR they disagreed three ways (1, 1, 3).

Who this actually changes

Only installs that never set the variable. ${VAR:-4} substitutes the default when WEB_SERVER_CONCURRENCY is unset or empty, so any explicit value is preserved:

WEB_SERVER_CONCURRENCY Workers before Workers after
unset 1 4
`` (set but empty) 1 4
1 1 1
8 8 8

So someone who deliberately pinned 1 on a small box keeps 1. The cost for everyone else is roughly 480 MB more RSS in exchange for the numbers above.

Running more than one worker is already a supported configuration rather than a new code path: start_log_forwarder in backend/endpoints/sockets/logs.py takes a Redis lock precisely so that "when more than one web worker is running (WEB_SERVER_CONCURRENCY > 1), exactly one forwards and clients don't receive duplicate lines".

Docs

docs/resources/snippets/env-vars.md in rommapp/docs is generated from this env.template by scripts/gen_env_vars.py, so it picks the new wording and default up on the next release bump. I confirmed the new line still parses under that generator's KEY=VALUE # COMMENT rules (two spaces before the # preserved, correct section, not flagged required).

Companion PR: rommapp/docs#130 hand-syncs the same row into the published table so the docs site is correct before that bump. It is optional; this PR is the actual fix.

Not changed

Each worker process gets its own SQLAlchemy QueuePool with the library defaults pool_size=5, max_overflow=10, never overridden in backend/handler/database/base_handler.py, so 15 connections per process. Four workers ceiling at 60 connections, plus 15 each for the RQ worker and RQ scan worker, which sits comfortably under MariaDB's default max_connections of 151. (At the old recommended 33 workers it would have ceilinged at 495, over-subscribed on paper, though pools only grow on demand and realistic bursts peaked at 14-17 connections even with 16 workers.) The new default has plenty of headroom, so I left the pool settings alone.

Fixes rommapp/docs#129

Checklist

  • I've tested the changes locally
  • I've updated relevant comments
  • I've assigned reviewers for this PR

AI disclosure

The benchmarking, analysis and wording in this PR were produced with Claude Code. I reviewed and verified every claim against the source before submitting.

🤖 Generated with Claude Code

"2 x CPU cores + 1" is gunicorn's guidance for sync workers. RomM runs
UvicornWorker with sync endpoints served from Starlette's threadpool, so
the rule does not transfer: on a 16-core host it measured 3x slower than
4 workers and cost 5.31 GB PSS.

The variable also only reaches the gunicorn API server; background tasks
run in the separate RQ worker processes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Updates the example environment configurations to replace the CPU-based web-worker heuristic with a four-worker recommendation.

  • Clarifies that WEB_SERVER_CONCURRENCY controls API worker processes rather than background workers.
  • Adds approximate per-worker memory guidance to both configuration examples.
  • Aligns the previously inconsistent values in the two files.

Confidence Score: 5/5

The documentation and example configuration changes appear safe to merge.

The revised values are consistent across both files, the variable’s documented scope matches its gunicorn usage, and no concrete behavioral or repository-rule violation remains.

Important Files Changed

Filename Overview
env.template Changes the template API-worker count from one to four and documents its scope and approximate memory cost.
examples/docker-compose.example.yml Aligns the Compose example with the four-worker recommendation and corrected API-worker guidance.

Reviews (1): Last reviewed commit: "docs(env): replace the WEB_SERVER_CONCUR..." | Re-trigger Greptile

Spinnich added a commit to Spinnich/docs that referenced this pull request Sep 7, 2026
"2 x CPU cores + 1" is gunicorn's guidance for sync workers and does not
apply to RomM's UvicornWorker setup. Mirrors rommapp/romm#4385 so the
published table is correct before the next pinned-ref bump regenerates it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A single worker was the reason a lone user loading a gallery measured
6.474 s worst-case TTFB against 1.939 s at 4 workers. The multi-worker
path is already supported: the log forwarder takes a Redis lock so
exactly one worker relays.

Only installs that never set WEB_SERVER_CONCURRENCY move; any explicit
value is preserved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Spinnich Spinnich changed the title docs(env): replace the WEB_SERVER_CONCURRENCY worker heuristic fix(server): default to 4 API workers and fix the WEB_SERVER_CONCURRENCY guidance Sep 7, 2026
@Spinnich
Spinnich requested a review from gantoine September 7, 2026 16:59
@gantoine
gantoine merged commit c5d5ec8 into rommapp:master Sep 7, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] WEB_SERVER_CONCURRENCY docs recommend "2 × CPU cores + 1", which is 3x slower than 4 workers and can exhaust RAM

2 participants