Configure uWSGI's Cheaper Subsystem for Dynamic Worker Scaling
A practical guide to configuring uWSGI's cheaper subsystem for dynamic worker scaling in Python WSGI applications, covering prerequisites, algorithm selection, tuning parameters, verification steps, and recovery from common misconfigurations.
08 Dec 2025, 10:15 UTC

Desired Outcome
Run a Python WSGI application under uWSGI with a worker pool that automatically shrinks during idle periods to save memory and expands under load to maintain throughput. The cheaper subsystem manages this by keeping a minimum number of workers (the cheaper value) and scaling up to a maximum (the workers value) based on busyness or queue depth.
Prerequisites
- uWSGI installed (tested with 2.0.x series; algorithm availability varies by release).
- A WSGI application entry point (e.g.,
myapp:app). - The master process enabled (
--master); cheaper mode cannot operate without it because the master spawns and reaps workers. - Sufficient file descriptor limits for the maximum worker count plus the listen queue.
Core Configuration
Start with a minimal working setup. Save the following as uwsgi.ini or pass the flags on the command line.
[uwsgi]
master = true
module = myapp:app
http-socket = :8080
# Maximum workers the pool may grow to
workers = 8
# Minimum workers kept alive when idle
cheaper = 2
# Algorithm that decides when to scale
cheaper-algo = spare
Run it:
uwsgi --ini uwsgi.ini
At startup you should see exactly two workers spawned. The master logs a line such as spawned uWSGI worker 1 (pid: 12345, cores: 1) for each initial worker.
Choosing a Scaling Algorithm
The cheaper-algo parameter selects the decision logic. Two common choices:
- spare (default): Adds a worker when all current workers are busy handling requests. Removes a worker after it has been idle for
cheaper-idleseconds. Simple and works well for request/response workloads. - backlog: Scales based on the listen queue depth. When the queue exceeds a threshold, workers are added; when it drains, workers are removed. Better suited for bursty traffic where queue length is a leading indicator.
Algorithm names and behavior are version-sensitive. Run uwsgi --cheaper-algo-list on your installed version to see available options.
Tuning Knobs
Adjust these parameters to match your application's startup cost and traffic pattern:
| Parameter | Meaning | Typical Starting Value |
|---|---|---|
cheaper-step | How many workers to add in a single scaling cycle | 1 |
cheaper-overload | Seconds a worker may stay overloaded (busy) before triggering scale-up | 3 |
cheaper-idle | Seconds a worker must be completely idle before it becomes eligible for removal | 10 |
cheaper-busy | Threshold (spare algo) or queue depth (backlog algo) that triggers scaling | algorithm-dependent |
Example tuned configuration:
[uwsgi]
master = true
module = myapp:app
http-socket = :8080
workers = 16
cheaper = 4
cheaper-algo = spare
cheaper-step = 2
cheaper-overload = 5
cheaper-idle = 30
With this setup, the pool starts at 4 workers. When all 4 are busy for 5 seconds, two more are spawned. After load subsides, workers that have been idle for 30 seconds are reaped until the pool returns to 4.
Interaction with Application Loading
Worker spawn cost depends on how the application is loaded:
- Preload (default): The application is loaded once in the master before forking. New workers inherit the loaded state, so spawn is fast (just a fork).
--lazy-apps: Each worker loads the application independently after fork. Spawn is slower because imports and initialization run in every worker. If you uselazy-apps, increasecheaper-overloadandcheaper-idleto avoid thrashing.
For apps with heavy imports (large ML models, extensive dependency trees), keep cheaper higher to avoid latency spikes when traffic resumes.
Expected Checks
Log Observation
Watch the uWSGI log during a load test. You should see lines like:
[cheaper] worker 3 buried after 30 seconds of inactivity
[cheaper] need more workers, spawning 2
Stats Server Verification
Enable the stats socket to query the current worker count programmatically:
[uwsgi]
stats = /tmp/uwsgi.stats
# ... other options
Query it with uwsgi --connect-and-read /tmp/uwsgi.stats or any JSON client. The workers array length reflects the live count. Under idle conditions it should equal cheaper; under sustained load it should approach workers.
Synthetic Load Test
Run a quick concurrent request loop (adjust concurrency to exceed cheaper):
for i in {1..50}; do curl -s http://localhost:8080/ & done; wait
While requests are in flight, the worker count should rise. After the loop finishes and cheaper-idle seconds elapse, the count should return to the minimum.
Common Misconfigurations and Recovery
Cheaper Greater Than Workers
If you set cheaper > workers, uWSGI logs an error and clamps the minimum to the maximum at startup. Fix the values and restart.
Missing Master Process
Running without --master silently disables cheaper scaling; the pool stays fixed at workers. Always verify master = true is present.
Conflict with External Process Managers
Do not combine aggressive cheaper settings with systemd, supervisord, or Kubernetes liveness probes that restart workers on their own. The two controllers will fight, causing rapid spawn/reap cycles. Choose one scaling authority.
Rollback Procedure
If a new cheaper configuration causes instability (e.g., workers thrashing, OOM kills):
- Stop uWSGI gracefully:
uwsgi --stop /tmp/uwsgi.pid(requirespidfileoption). - Revert to the previous known-good INI file.
- Restart:
uwsgi --ini uwsgi.ini. - Confirm the worker count stabilizes at the expected minimum.
Limitations
- Cheaper scaling is reactive; it cannot anticipate traffic spikes. For predictable bursts, consider a fixed pool or external autoscaler.
- The stats server exposes internal state but does not provide a scaling API; you cannot command a specific worker count via the socket.
- Algorithm behavior may change between uWSGI releases. Pin your uWSGI version in deployment manifests and re-verify after upgrades.
Quick Reference Checklist
master = trueenabledcheaper≤workerscheaper-algoselected and supported by installed versioncheaper-idlelong enough to avoid thrashing (start at 30s)- Stats socket enabled for monitoring
- Load test confirms scale-up and scale-down
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.