Choosing between a fixed uWSGI worker pool and the cheaper subsystem
Decide when to use a fixed uWSGI worker pool versus the cheaper dynamic scaling subsystem for Python WSGI apps.
20 Sept 2025, 15:48 UTC

The decision: fixed pool or cheaper scaling
You run a Python WSGI application under uWSGI and need to decide how many worker processes to keep alive. If traffic is steady, a fixed pool is simpler and easier to recycle. If traffic arrives in bursts, keeping the maximum number of workers idle wastes memory. uWSGI's cheaper subsystem can spawn and reap workers dynamically, but it cannot create capacity faster than a worker can import your application, and it adds operational complexity.
Use a fixed worker pool when load is predictable or memory is the binding constraint. Use the cheaper subsystem only when traffic is bursty and idle workers consume meaningful RAM, and only after you have measured the spawn cost of your application.
Constraints and version assumptions
This guide assumes a standard uWSGI build serving a Python WSGI callable, typically behind a reverse proxy such as nginx. The cheaper-* options and the stats JSON schema are version-sensitive. Pin the uWSGI version in your image and confirm available options with uwsgi --help | grep -i cheaper before deploying. Do not copy option names from a different release without checking your installed binary.
Important terms: worker is a uWSGI process that runs your WSGI app. Backlog is the listen queue of accepted-but-not-yet-handled connections. RSS is the resident set size, the physical memory a process uses. GIL is the Python global interpreter lock; with enable-threads, each worker still handles one request at a time unless your app releases the lock.
Supported options compared
| Option | Best for | Behavior | Main risk |
|---|---|---|---|
Fixed pool (processes=N) | Steady load, predictable memory | Keeps exactly N workers running. | No idle memory savings; over-provisioning wastes RAM. |
cheaper with cheaper-algo=backlog | Bursty connection volume | Spawns workers when the listen backlog grows. | Spawn latency; if cheaper=0 and cheaper-initial=0, no workers exist until the backlog fills. |
cheaper with cheaper-algo=spare plus cheaper-idle | Steady CPU, variable concurrency | Spawns when idle workers run out; reaps workers after N idle seconds. | Worker identity changes, complicating per-worker state and graceful reloads. |
Key related settings: cheaper-initial sets workers at startup; cheaper-step controls how many workers are added or removed at once; processes is the hard ceiling; listen sets the backlog depth; max-requests recycles workers; reload-on-rss restarts workers that grow too large.
Trade-offs
- Memory versus latency. The cheaper subsystem saves RAM only at low load. Every new worker imports your application unless preloading shares memory, so scale-up is limited by import time. Pair cheaper scaling with a larger listen backlog and a reverse-proxy timeout that tolerates the delay.
- Worker identity. Cheaper reaping makes worker PIDs less stable. Avoid storing per-worker state in memory, and expect graceful reloads to behave differently than with a fixed pool.
- Recycling interactions.
max-requestsand idle reaping can remove a worker before it reaches its request limit. Do not rely onmax-requestsalone for memory hygiene. - Emperor mode. In Emperor mode,
cheapergoverns workers inside each vassal. The Emperor still spawns and kills whole vassals, so do not configure both layers to scale independently.
Concrete configuration
This example runs a WSGI application at /srv/app/wsgi.py exposing app. Adjust paths and the module name for your project. Run the command as the application user, not root, unless you must bind to a port below 1024.
[uwsgi]
module = app:app
chdir = /srv/app
master = 1
processes = 16
cheaper = 4
cheaper-initial = 4
cheaper-step = 4
cheaper-algo = backlog
cheaper-idle = 30
listen = 128
stats = 127.0.0.1:1717
max-requests = 5000
reload-on-rss = 512
daemonize = /var/log/uwsgi/app.log
pidfile = /run/uwsgi/app.pid
Where to run: a shell on the application host, as the user that owns /srv/app.
uwsgi --ini /srv/app/uwsgi.ini
Expected checks: the master process starts, workers appear in the process list, and /var/log/uwsgi/app.log shows worker initialization without tracebacks. Risks: setting cheaper equal to processes disables scaling; setting cheaper above processes is clamped to the ceiling. Binding the stats socket to a public interface exposes internal metrics; keep it on loopback or a restricted Unix socket.
Validate with the stats socket
Enable the stats socket as shown above, then query it from the same host:
curl -s http://127.0.0.1:1717 | python3 -m json.tool
The exact JSON fields vary by uWSGI version. Inspect the payload for worker count, requests, and RSS before scripting alerts. Do not assume field names from another release.
Drive load with a tool such as wrk, hey, or ab from a separate host, then sample the stats JSON every few seconds. Confirm that:
- Worker count climbs toward
processesunder sustained load. - Worker count falls back toward
cheaperaftercheaper-idleseconds of quiet. - RSS drops as idle workers are reaped.
- uWSGI logs contain worker spawn and kill lines matching the chosen algorithm.
Practical diagnostic decision: if the accept queue depth grows while workers are idle, the bottleneck is likely the backlog, favoring cheaper-algo=backlog. If workers are busy but idle workers are exhausted, cheaper-algo=spare is more relevant. Match the algorithm to your observed bottleneck rather than choosing by preference.
Limitations
- Cheaper scaling cannot help when all workers are busy; it only reduces idle count.
- Each spawned worker pays the application import cost unless your build shares memory through preloading.
- The stats schema and cheaper option names change across uWSGI versions; treat this configuration as version-specific.
- This guide does not cover threading, async mode, or uWSGI mules.
Rollback
Because editing uwsgi.ini changes running state, keep a previous known-good file. To rollback, restore the earlier ini, stop the current master (for example, kill $(cat /run/uwsgi/app.pid) or your service manager's stop command), and start uWSGI with the restored file. Verify that worker count returns to the previous baseline.
Version check before deploy
uwsgi --version
uwsgi --help | grep -i cheaper
If the expected options are absent or behave differently, adjust this guide to your installed release before using it in production.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.