Skip to content

Comments

feat: configurable web worker count via WEB_WORKERS env var#1145

Closed
mihow wants to merge 1 commit intomainfrom
feat/web-workers
Closed

feat: configurable web worker count via WEB_WORKERS env var#1145
mihow wants to merge 1 commit intomainfrom
feat/web-workers

Conversation

@mihow
Copy link
Collaborator

@mihow mihow commented Feb 21, 2026

Summary

  • Local dev start script supports WEB_WORKERS env var; values > 1 run uvicorn with --workers (disables --reload since they're incompatible)
  • Production start script passes explicit --workers to gunicorn (defaults to 4)

Single-worker mode (default) is unchanged — keeps --reload for hot reloading in dev.

Context

During PSv2 integration testing, a single uvicorn worker couldn't handle concurrent requests from multiple DataLoader subprocesses. This makes it easy to run multiple workers locally with WEB_WORKERS=2 docker compose up django.

Test plan

  • docker compose up django — single worker with --reload (default, unchanged)
  • WEB_WORKERS=2 docker compose up django — two workers, no --reload
  • DEBUGGER=1 docker compose up django — debugpy mode unchanged

Summary by CodeRabbit

  • Chores
    • Development and production servers now support configurable worker scaling via environment variable, enabling flexible performance tuning based on deployment needs.

Local dev: WEB_WORKERS > 1 runs uvicorn with --workers (no --reload).
Default single-worker mode keeps --reload for hot reloading.
Production: explicit --workers for gunicorn (defaults to 4).

Co-Authored-By: Claude <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings February 21, 2026 00:57
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 21, 2026

📝 Walkthrough

Walkthrough

The startup scripts for local and production Django environments are enhanced to support configurable web server workers via the WEB_WORKERS environment variable. Local development conditionally enables reload behavior based on worker count, while production dynamically configures Gunicorn workers.

Changes

Cohort / File(s) Summary
Worker Configuration
compose/local/django/start, compose/production/django/start
Both scripts now read WEB_WORKERS env var to configure parallelism. Local dev conditionally applies --reload (single worker) or --workers flag (multiple workers). Production Gunicorn dynamically sets worker count (default 4).

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

Poem

🐰 One worker watches code transform,
Many workers brave the storm,
Environment whispers how to scale,
Reload or rest—the startup tale! 🚀

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: configurable web worker count via WEB_WORKERS env var' accurately and concisely describes the main change—adding the WEB_WORKERS environment variable to configure worker count in both development and production startup scripts.
Description check ✅ Passed The description includes a summary of changes, context for the change, and a test plan. However, it is missing several template sections: the 'List of Changes' bullet points, 'Related Issues' reference, detailed deployment notes, and a completion checklist.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/web-workers

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@netlify
Copy link

netlify bot commented Feb 21, 2026

Deploy Preview for antenna-preview ready!

Name Link
🔨 Latest commit ac06d78
🔍 Latest deploy log https://app.netlify.com/projects/antenna-preview/deploys/699902f3294fd00008c8dc52
😎 Deploy Preview https://deploy-preview-1145--antenna-preview.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 60 (🔴 down 6 from production)
Accessibility: 80 (no change from production)
Best Practices: 92 (🔴 down 8 from production)
SEO: 92 (no change from production)
PWA: 80 (no change from production)
View the detailed breakdown and full score reports

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify
Copy link

netlify bot commented Feb 21, 2026

Deploy Preview for antenna-ssec ready!

Name Link
🔨 Latest commit ac06d78
🔍 Latest deploy log https://app.netlify.com/projects/antenna-ssec/deploys/699902f3a161ed00087f04d7
😎 Deploy Preview https://deploy-preview-1145--antenna-ssec.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
compose/local/django/start (1)

14-15: Consider guarding against non-integer WEB_WORKERS for a clearer error.

If WEB_WORKERS is set to a non-integer value (e.g. "two", "2.5"), [ "$WORKERS" -gt 1 ] will fail with integer expression expected and set -o errexit will exit silently. A short guard produces a much friendlier message.

🛡️ Proposed guard
     WORKERS="${WEB_WORKERS:-1}"
+    if ! [[ "$WORKERS" =~ ^[0-9]+$ ]]; then
+        echo "Error: WEB_WORKERS must be a non-negative integer (got: '$WORKERS')" >&2
+        exit 1
+    fi
     if [ "$WORKERS" -gt 1 ]; then
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@compose/local/django/start` around lines 14 - 15, The current check using [
"$WORKERS" -gt 1 ] will fail if WEB_WORKERS contains non-integer text; modify
the start-up script to validate WORKERS is an integer before the numeric
comparison by testing WORKERS against a digits-only pattern (e.g. a regex like
^[0-9]+$ or a shell case) and if it doesn’t match print a clear error mentioning
WEB_WORKERS and exit non‑zero; after the guard you can safely use the existing [
"$WORKERS" -gt 1 ] branch.
compose/production/django/start (2)

10-10: Implicit default jump from 1 → 4 workers will affect all existing deployments.

Previously, gunicorn ran without --workers, which defaults to 1 worker. Any deployment that does not explicitly set WEB_WORKERS will now start 4 workers automatically. This silently quadruples:

  • Memory consumed per container instance.
  • Database connections opened (each worker holds its own connection pool).

Ensure your DB connection limit and container memory limits can accommodate 4× the prior baseline before rolling this out.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@compose/production/django/start` at line 10, The change adds a hard default
of 4 workers via the --workers flag which silently multiplies memory and DB
connections for deployments that don't set WEB_WORKERS; update the start command
so it does not unconditionally default to 4—either omit the --workers flag when
WEB_WORKERS is unset or default WEB_WORKERS to 1 instead; locate the shell
invocation using the --workers flag and the WEB_WORKERS expansion and change it
to only emit --workers when WEB_WORKERS is set (or set the expansion to
"${WEB_WORKERS:-1}" if you intend an explicit 1-worker default).

10-10: WEB_WORKERS accepts any string — consider guarding against non-integer input.

${WEB_WORKERS:-4} passes the value verbatim to gunicorn. If WEB_WORKERS is set to a non-numeric or zero/negative value, gunicorn will fail at startup with a cryptic error rather than a clear configuration message.

🛡️ Optional: add a validation guard
+WEB_WORKERS="${WEB_WORKERS:-4}"
+if ! [[ "${WEB_WORKERS}" =~ ^[1-9][0-9]*$ ]]; then
+  echo "ERROR: WEB_WORKERS must be a positive integer (got: '${WEB_WORKERS}')" >&2
+  exit 1
+fi
+
 exec newrelic-admin run-program /usr/local/bin/gunicorn config.asgi \
-    --workers "${WEB_WORKERS:-4}" \
+    --workers "${WEB_WORKERS}" \
     --bind 0.0.0.0:5000 --chdir=/app -k uvicorn.workers.UvicornWorker
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@compose/production/django/start` at line 10, Guard against non-integer or
non-positive WEB_WORKERS before passing it to gunicorn: validate the WEB_WORKERS
environment variable in the start script (the place that builds the --workers
"${WEB_WORKERS:-4}" argument), ensure it is a positive integer, and fall back to
4 (or exit with a clear error) if validation fails; update the start script to
parse and coerce/validate WEB_WORKERS (e.g., regex/digit check or integer
conversion) and only pass a validated positive integer to gunicorn's --workers
flag.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@compose/local/django/start`:
- Around line 14-15: The current check using [ "$WORKERS" -gt 1 ] will fail if
WEB_WORKERS contains non-integer text; modify the start-up script to validate
WORKERS is an integer before the numeric comparison by testing WORKERS against a
digits-only pattern (e.g. a regex like ^[0-9]+$ or a shell case) and if it
doesn’t match print a clear error mentioning WEB_WORKERS and exit non‑zero;
after the guard you can safely use the existing [ "$WORKERS" -gt 1 ] branch.

In `@compose/production/django/start`:
- Line 10: The change adds a hard default of 4 workers via the --workers flag
which silently multiplies memory and DB connections for deployments that don't
set WEB_WORKERS; update the start command so it does not unconditionally default
to 4—either omit the --workers flag when WEB_WORKERS is unset or default
WEB_WORKERS to 1 instead; locate the shell invocation using the --workers flag
and the WEB_WORKERS expansion and change it to only emit --workers when
WEB_WORKERS is set (or set the expansion to "${WEB_WORKERS:-1}" if you intend an
explicit 1-worker default).
- Line 10: Guard against non-integer or non-positive WEB_WORKERS before passing
it to gunicorn: validate the WEB_WORKERS environment variable in the start
script (the place that builds the --workers "${WEB_WORKERS:-4}" argument),
ensure it is a positive integer, and fall back to 4 (or exit with a clear error)
if validation fails; update the start script to parse and coerce/validate
WEB_WORKERS (e.g., regex/digit check or integer conversion) and only pass a
validated positive integer to gunicorn's --workers flag.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request adds support for configurable web worker count via the WEB_WORKERS environment variable to enable handling concurrent requests during local development and production deployments. The feature addresses a limitation where a single uvicorn worker couldn't handle concurrent requests from multiple DataLoader subprocesses during PSv2 integration testing.

Changes:

  • Local development script conditionally enables multiple uvicorn workers when WEB_WORKERS > 1, disabling hot-reload since it's incompatible with multi-worker mode
  • Production script explicitly passes --workers flag to gunicorn with a default of 4 workers
  • Single-worker mode remains the default for local development, preserving hot-reload functionality

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
compose/production/django/start Added --workers flag to gunicorn command with default value of 4 from WEB_WORKERS env var
compose/local/django/start Added conditional logic to enable multiple uvicorn workers when WEB_WORKERS > 1, disabling --reload in multi-worker mode

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

exec python -Xfrozen_modules=off -m debugpy --listen 0.0.0.0:5678 -m uvicorn config.asgi:application --host 0.0.0.0
else
exec uvicorn config.asgi:application --host 0.0.0.0 --reload --reload-include '*.html'
WORKERS="${WEB_WORKERS:-1}"
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The numeric comparison on line 15 will fail with a cryptic error if WEB_WORKERS is set to a non-numeric value (e.g., WEB_WORKERS=abc). Consider adding input validation before the comparison to ensure WORKERS is a positive integer, or use a more defensive comparison that handles non-numeric values gracefully.

Suggested change
WORKERS="${WEB_WORKERS:-1}"
WORKERS="${WEB_WORKERS:-1}"
# Ensure WORKERS is a positive integer; fall back to 1 if invalid
if ! [[ "$WORKERS" =~ ^[0-9]+$ ]] || [ "$WORKERS" -lt 1 ]; then
echo "Invalid WEB_WORKERS value '$WORKERS'. Falling back to 1 worker." >&2
WORKERS=1
fi

Copilot uses AI. Check for mistakes.
Comment on lines +9 to +10
exec newrelic-admin run-program /usr/local/bin/gunicorn config.asgi \
--workers "${WEB_WORKERS:-4}" \
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If WEB_WORKERS is set to a non-numeric value (e.g., WEB_WORKERS=abc) or zero/negative value, gunicorn will fail. Consider adding input validation to ensure WEB_WORKERS is a positive integer before using it, which would provide a clearer error message.

Suggested change
exec newrelic-admin run-program /usr/local/bin/gunicorn config.asgi \
--workers "${WEB_WORKERS:-4}" \
# Validate WEB_WORKERS as a positive integer, defaulting to 4 if unset or invalid
WEB_WORKERS_VALUE="${WEB_WORKERS:-4}"
if ! [[ "${WEB_WORKERS_VALUE}" =~ ^[1-9][0-9]*$ ]]; then
echo "Invalid WEB_WORKERS value '${WEB_WORKERS:-unset}'. Must be a positive integer. Falling back to 4." >&2
WEB_WORKERS_VALUE=4
fi
exec newrelic-admin run-program /usr/local/bin/gunicorn config.asgi \
--workers "${WEB_WORKERS_VALUE}" \

Copilot uses AI. Check for mistakes.
Comment on lines +14 to +17
WORKERS="${WEB_WORKERS:-1}"
if [ "$WORKERS" -gt 1 ]; then
# --reload is incompatible with --workers, so skip it for multi-worker mode
exec uvicorn config.asgi:application --host 0.0.0.0 --workers "$WORKERS"
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding documentation for the WEB_WORKERS environment variable. Based on the pattern in CLAUDE.md where DEBUGGER=1 is documented (lines 96-106), this new environment variable should also be documented to help developers understand how to use it for local development with multiple workers.

Copilot uses AI. Check for mistakes.
@mihow
Copy link
Collaborator Author

mihow commented Feb 21, 2026

Merging worker config from #1142 and #1145

After investigating production, here's what I found and a plan to consolidate.

Discovery: WEB_CONCURRENCY is already the standard

Gunicorn natively reads WEB_CONCURRENCY as the default for --workers. Our production env file already sets WEB_CONCURRENCY=4, which is why we already have 4 workers running — even without any --workers flag in the start script.

So both WEB_WORKERS (this PR) and GUNICORN_WORKERS (#1142) are reinventing a built-in. The standard name, used by Heroku, Render, Railway, and gunicorn itself, is WEB_CONCURRENCY.

Production server resources

  • 16 vCPUs (Xeon Skylake), 32 GiB RAM — mostly idle
  • Currently running 4 UvicornWorker processes (via WEB_CONCURRENCY=4)
  • Plenty of headroom to increase

Plan

For the start scripts, the minimal change is:

  1. Use WEB_CONCURRENCY — no custom env var needed. Gunicorn reads it natively, so we don't even need to pass --workers explicitly. But making it explicit in the script is fine for clarity.
  2. Auto-detect fallback (from PSv2: Improve task fetching & web worker concurrency configuration #1142): When WEB_CONCURRENCY is not set, calculate a sensible default. But use an ASGI-appropriate formula — nproc * 2 + 1 is for sync WSGI workers. For async UvicornWorker, something like nproc (capped at maybe 12) is more reasonable. On our 16-core prod box, nproc * 2 + 1 = 33 would be excessive.
  3. Local dev (from feat: configurable web worker count via WEB_WORKERS env var #1145): Keep the single-worker + --reload path as default for dev. The conditional logic in feat: configurable web worker count via WEB_WORKERS env var #1145 (reload when 1 worker, no reload when >1) is the right approach.

For horizontal scaling (Traefik), this is ready when we need it:

  • compose/production/traefik/traefik.yml already exists and is configured to load-balance to http://django:5000
  • To enable: add a traefik service to docker-compose.production.yml, remove the ports: "5001:5000" from django, and scale with --scale django=N
  • This gives process isolation (one crash doesn't kill all workers) and per-container memory visibility
  • Not urgent since gunicorn workers within a single container handle our current load fine, but it's a natural next step if we need more isolation or want to scale across machines

I'll update #1142 to use WEB_CONCURRENCY and incorporate the local dev improvements from this PR so we can close this one in favor of a unified approach there.

@mihow
Copy link
Collaborator Author

mihow commented Feb 21, 2026

Closing in favor of #1142 which now uses the standard WEB_CONCURRENCY env var (gunicorn reads it natively). The USE_UVICORN=1 escape hatch is available for local dev if needed.

@mihow mihow closed this Feb 21, 2026
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.

1 participant