diff --git a/docker-example/app-platform-backend/README.md b/docker-example/app-platform-backend/README.md index 75365b8a2dd..c693719da81 100644 --- a/docker-example/app-platform-backend/README.md +++ b/docker-example/app-platform-backend/README.md @@ -38,8 +38,9 @@ The backend is built by the `Dockerfile` in this directory. When deploying the backend, be sure to set REFLEX_REDIS_URL=redis://internal-redis-hostname to connect to the redis service. -With redis available, each replica runs `2 * cpu_count + 1` worker processes. -Set `GRANIAN_WORKERS` to cap this on small instance sizes. +With redis available, each replica runs up to 8 workers by default +(`2 * cpu_count + 1`, capped at 8). When using Granian, set `GRANIAN_WORKERS` +to override the default, including to use more workers. If the app uses postgres, add `psycopg[binary]` to `requirements.txt`; the binary wheel bundles libpq, so no extra system packages are needed in the image. diff --git a/docker-example/production-compose/README.md b/docker-example/production-compose/README.md index bcb88be4789..60159775696 100644 --- a/docker-example/production-compose/README.md +++ b/docker-example/production-compose/README.md @@ -68,9 +68,9 @@ Add `psycopg[binary]` to `requirements.txt` so the backend can connect to postgres; the binary wheel bundles libpq, so no extra system packages are needed in the image. -With redis available, the backend runs `2 * cpu_count + 1` worker processes. -Set `GRANIAN_WORKERS` in the `app` environment to cap this on memory -constrained hosts. +With redis available, the backend runs up to 8 workers by default (`2 * cpu_count + 1`, +capped at 8). When using Granian, set `GRANIAN_WORKERS` in the `app` environment +to override the default, including to use more workers. Postgres uses its own named docker volume for data persistence. diff --git a/docker-example/production/README.md b/docker-example/production/README.md index 8515678e66a..91e3ea04443 100644 --- a/docker-example/production/README.md +++ b/docker-example/production/README.md @@ -31,8 +31,8 @@ To listen on a different port, pass `--build-arg PORT=10000`. docker run -p 8080:8080 reflex-production ``` -By default the backend runs `2 * cpu_count + 1` workers. Set `GRANIAN_WORKERS` -to a smaller number in memory constrained environments: +By default the backend runs up to 8 workers (`2 * cpu_count + 1`, capped at 8). +When using Granian, set `GRANIAN_WORKERS` to override the default, including to use more workers: ```console docker run -e GRANIAN_WORKERS=2 -p 8080:8080 reflex-production diff --git a/docs/hosting/self-hosting.md b/docs/hosting/self-hosting.md index cd32acdada1..8ef5b76e006 100644 --- a/docs/hosting/self-hosting.md +++ b/docs/hosting/self-hosting.md @@ -44,8 +44,8 @@ Production mode compiles the app, builds an optimized static frontend, and serves it together with the backend (event websocket, `/ping`, `/_upload`) from a single server on port `3000`. Pass `--frontend-port` or `--backend-port` to listen on a different port. When redis is configured, -the server runs `2 * cpu_count + 1` worker processes; set `GRANIAN_WORKERS` -to override this. +the server runs up to 8 backend workers by default (`2 * cpu_count + 1`, capped at 8). +When using Granian, set `GRANIAN_WORKERS` to override the default, including to use more workers. The frontend and backend can also run as separate processes, for example to serve the frontend from a CDN and scale the backend independently. The diff --git a/news/+cap-default-backend-workers.bugfix.md b/news/+cap-default-backend-workers.bugfix.md new file mode 100644 index 00000000000..89874b66ed1 --- /dev/null +++ b/news/+cap-default-backend-workers.bugfix.md @@ -0,0 +1 @@ +Cap the automatically selected number of Redis-backed backend workers at eight. Set `GRANIAN_WORKERS` to override the default. \ No newline at end of file diff --git a/reflex/utils/processes.py b/reflex/utils/processes.py index 934bc37b774..2e5d449c582 100644 --- a/reflex/utils/processes.py +++ b/reflex/utils/processes.py @@ -29,6 +29,8 @@ logger = logging.getLogger(__name__) +MAX_BACKEND_WORKERS = 8 + def kill(pid: int): """Kill a process. @@ -61,7 +63,7 @@ def get_num_workers() -> int: except RedisError as re: logger.error(f"Unable to connect to Redis: {re}") raise SystemExit(1) from None - return (os.cpu_count() or 1) * 2 + 1 + return min((os.cpu_count() or 1) * 2 + 1, MAX_BACKEND_WORKERS) def _can_bind_at_port( diff --git a/tests/units/utils/test_processes.py b/tests/units/utils/test_processes.py index e16e321051b..b4917b258bd 100644 --- a/tests/units/utils/test_processes.py +++ b/tests/units/utils/test_processes.py @@ -29,6 +29,27 @@ ) +@pytest.mark.parametrize( + ("cpu_count", "expected"), + [(None, 3), (0, 3), (1, 3), (2, 5), (3, 7), (4, 8), (64, 8)], +) +def test_get_num_workers_caps_automatic_worker_count(cpu_count, expected): + """Cap automatic worker counts while preserving the CPU-based default below the cap.""" + redis_client = mock.Mock() + with ( + mock.patch.object( + processes, "get_config", return_value=mock.Mock(transport="websocket") + ), + mock.patch.object( + processes.prerequisites, "get_redis_sync", return_value=redis_client + ), + mock.patch.object(processes.os, "cpu_count", return_value=cpu_count), + ): + assert processes.get_num_workers() == expected + + redis_client.ping.assert_called_once_with() + + def test_is_process_on_port_free_port(): """Test is_process_on_port returns False when port is free.""" # Find a free port