Skip to content
Merged
5 changes: 3 additions & 2 deletions docker-example/app-platform-backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 3 additions & 3 deletions docker-example/production-compose/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
4 changes: 2 additions & 2 deletions docker-example/production/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions docs/hosting/self-hosting.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 worker processes by default (`2 * cpu_count + 1`, capped at 8).
Comment thread
harsh21234i marked this conversation as resolved.
Outdated
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
Expand Down
1 change: 1 addition & 0 deletions news/+cap-default-backend-workers.bugfix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Cap the automatically selected number of Redis-backed backend workers at eight. Set `GRANIAN_WORKERS` to override the default.
4 changes: 3 additions & 1 deletion reflex/utils/processes.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@

logger = logging.getLogger(__name__)

MAX_BACKEND_WORKERS = 8


def kill(pid: int):
"""Kill a process.
Expand Down Expand Up @@ -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(
Expand Down
21 changes: 21 additions & 0 deletions tests/units/utils/test_processes.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading