From 19123c4111e20850c66805dc116396a4c72149b8 Mon Sep 17 00:00:00 2001 From: kirtimanmishrazipstack Date: Thu, 13 Aug 2026 13:53:25 +0530 Subject: [PATCH 1/4] UN-4001 [FIX] Stop Socket.IO WebSockets from exhausting the Gunicorn thread pool The Socket.IO WSGI app runs with async_mode="threading", so every open WebSocket holds one gthread pool thread for that connection's lifetime rather than for a single request. With --threads 2, two browser tabs landing on the same worker left it with no threads for HTTP, and the worker kept accepting connections it could never read. Make workers, threads and worker-connections environment-configurable and raise the default thread ceiling to 512, matching the value already used by the cloud chart. The pool spawns threads lazily, so the higher ceiling costs nothing at idle. Drive --log-level from DEFAULT_LOG_LEVEL instead of hardcoding debug, and state --worker-class gthread explicitly. Co-Authored-By: Claude Opus 5 (1M context) --- backend/entrypoint.sh | 14 ++++++++++---- backend/sample.env | 9 ++++++++- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/backend/entrypoint.sh b/backend/entrypoint.sh index 5d44345fda..733f229638 100755 --- a/backend/entrypoint.sh +++ b/backend/entrypoint.sh @@ -29,12 +29,18 @@ if [ "$migrate" = true ]; then .venv/bin/python manage.py migrate fi -# Configure Gunicorn based on --dev flag +# Configure Gunicorn based on --dev flag. +# Threads must exceed the number of concurrently connected browser tabs: the +# Socket.IO WSGI app runs with async_mode="threading", so each open WebSocket +# holds one gthread pool thread for that connection's lifetime, not just for a +# request. The pool spawns threads lazily, so a high ceiling is free at idle. gunicorn_args=( --bind 0.0.0.0:8000 - --workers 2 - --threads 2 - --log-level debug + --workers "${GUNICORN_WORKERS:-2}" + --threads "${GUNICORN_THREADS:-512}" + --worker-class gthread + --worker-connections "${GUNICORN_WORKER_CONNECTIONS:-1000}" + --log-level "${DEFAULT_LOG_LEVEL:-INFO}" --timeout 600 --access-logfile - ) diff --git a/backend/sample.env b/backend/sample.env index b57101813e..ed23f4ec38 100644 --- a/backend/sample.env +++ b/backend/sample.env @@ -4,9 +4,16 @@ DJANGO_SETTINGS_MODULE='backend.settings.dev' SESSION_COOKIE_SECURE=False CSRF_COOKIE_SECURE=False -# Default log level +# Default log level. Also drives Gunicorn's --log-level. DEFAULT_LOG_LEVEL="INFO" +# Gunicorn tuning. GUNICORN_THREADS must exceed the number of concurrently +# connected browser tabs — each open Socket.IO WebSocket holds one thread for +# its whole lifetime. +# GUNICORN_WORKERS=2 +# GUNICORN_THREADS=512 +# GUNICORN_WORKER_CONNECTIONS=1000 + # Common PATH_PREFIX="api/v1" From 782f69abc9cd07338a70ad7dba89230d8f0977b5 Mon Sep 17 00:00:00 2001 From: kirtimanmishrazipstack Date: Mon, 17 Aug 2026 12:13:29 +0530 Subject: [PATCH 2/4] UN-3996 [FIX] Drop the Gunicorn log level change from the thread fix Revert --log-level back to the hardcoded debug so this change is scoped to the thread pool alone. Reusing DEFAULT_LOG_LEVEL conflated Django's application log level with Gunicorn's, which are independent. Co-Authored-By: Claude Opus 5 --- backend/entrypoint.sh | 9 ++++----- backend/sample.env | 7 +++---- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/backend/entrypoint.sh b/backend/entrypoint.sh index 733f229638..0b77323286 100755 --- a/backend/entrypoint.sh +++ b/backend/entrypoint.sh @@ -30,17 +30,16 @@ if [ "$migrate" = true ]; then fi # Configure Gunicorn based on --dev flag. -# Threads must exceed the number of concurrently connected browser tabs: the -# Socket.IO WSGI app runs with async_mode="threading", so each open WebSocket -# holds one gthread pool thread for that connection's lifetime, not just for a -# request. The pool spawns threads lazily, so a high ceiling is free at idle. +# Threads must exceed concurrent browser tabs — each open Socket.IO WebSocket +# holds one thread for its lifetime. The pool spawns threads lazily, so a high +# ceiling is free at idle. gunicorn_args=( --bind 0.0.0.0:8000 --workers "${GUNICORN_WORKERS:-2}" --threads "${GUNICORN_THREADS:-512}" --worker-class gthread --worker-connections "${GUNICORN_WORKER_CONNECTIONS:-1000}" - --log-level "${DEFAULT_LOG_LEVEL:-INFO}" + --log-level debug --timeout 600 --access-logfile - ) diff --git a/backend/sample.env b/backend/sample.env index ed23f4ec38..b32050f16a 100644 --- a/backend/sample.env +++ b/backend/sample.env @@ -4,12 +4,11 @@ DJANGO_SETTINGS_MODULE='backend.settings.dev' SESSION_COOKIE_SECURE=False CSRF_COOKIE_SECURE=False -# Default log level. Also drives Gunicorn's --log-level. +# Default log level DEFAULT_LOG_LEVEL="INFO" -# Gunicorn tuning. GUNICORN_THREADS must exceed the number of concurrently -# connected browser tabs — each open Socket.IO WebSocket holds one thread for -# its whole lifetime. +# Gunicorn tuning. GUNICORN_THREADS must exceed concurrent browser tabs — +# each open Socket.IO WebSocket holds one thread for its lifetime. # GUNICORN_WORKERS=2 # GUNICORN_THREADS=512 # GUNICORN_WORKER_CONNECTIONS=1000 From 7099af95da67916bcf6d8919291306057bb311e6 Mon Sep 17 00:00:00 2001 From: kirtimanmishrazipstack Date: Mon, 17 Aug 2026 13:18:50 +0530 Subject: [PATCH 3/4] UN-3996 [FIX] Set the Gunicorn env vars live in sample.env MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit They were commented out, unlike the other 110 settings in the file. Leaving them commented hid the knob, which was the original complaint — there was no discoverable way to change the thread count. Co-Authored-By: Claude Opus 5 --- backend/sample.env | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/sample.env b/backend/sample.env index b32050f16a..5c21e428fa 100644 --- a/backend/sample.env +++ b/backend/sample.env @@ -9,9 +9,9 @@ DEFAULT_LOG_LEVEL="INFO" # Gunicorn tuning. GUNICORN_THREADS must exceed concurrent browser tabs — # each open Socket.IO WebSocket holds one thread for its lifetime. -# GUNICORN_WORKERS=2 -# GUNICORN_THREADS=512 -# GUNICORN_WORKER_CONNECTIONS=1000 +GUNICORN_WORKERS=2 +GUNICORN_THREADS=512 +GUNICORN_WORKER_CONNECTIONS=1000 # Common PATH_PREFIX="api/v1" From e1e82c5a41a3bdf705bab95c6b14345256f175d3 Mon Sep 17 00:00:00 2001 From: kirtimanmishrazipstack Date: Mon, 17 Aug 2026 13:21:29 +0530 Subject: [PATCH 4/4] UN-3996 [FIX] Drop the redundant worker-connections knob 1000 is already Gunicorn's default for worker_connections, so passing it changed nothing. Making it configurable only added a way to set it below GUNICORN_THREADS, which silently disables HTTP keep-alive. Co-Authored-By: Claude Opus 5 --- backend/entrypoint.sh | 1 - backend/sample.env | 1 - 2 files changed, 2 deletions(-) diff --git a/backend/entrypoint.sh b/backend/entrypoint.sh index 0b77323286..20441c58fb 100755 --- a/backend/entrypoint.sh +++ b/backend/entrypoint.sh @@ -38,7 +38,6 @@ gunicorn_args=( --workers "${GUNICORN_WORKERS:-2}" --threads "${GUNICORN_THREADS:-512}" --worker-class gthread - --worker-connections "${GUNICORN_WORKER_CONNECTIONS:-1000}" --log-level debug --timeout 600 --access-logfile - diff --git a/backend/sample.env b/backend/sample.env index 5c21e428fa..700422d1a4 100644 --- a/backend/sample.env +++ b/backend/sample.env @@ -11,7 +11,6 @@ DEFAULT_LOG_LEVEL="INFO" # each open Socket.IO WebSocket holds one thread for its lifetime. GUNICORN_WORKERS=2 GUNICORN_THREADS=512 -GUNICORN_WORKER_CONNECTIONS=1000 # Common PATH_PREFIX="api/v1"