The uWSGI cheaper subsystem dynamically scales the worker pool up and down at runtime based on demand. The master process spawns additional workers when traffic increases and reaps them when it subsides, reducing memory consumption during idle periods and providing automatic capacity during bursts.
When the cheaper subsystem is active, the number of alive workers fluctuates by design. Cheaped workers (those scaled down by the subsystem) appear in the stats server output with "status":"cheap" and "pid":0. Monitoring that expects a fixed worker count will fire false “missing workers” alerts every time the system scales down.
What it is and why it matters
Instead of --processes 8 always running eight workers regardless of load, you configure a range: a minimum (--cheaper) and a maximum (--workers or --processes). The master process adjusts the live worker count within this range based on the selected algorithm.
For example, with --cheaper 2 --processes 16, the system starts with 2 workers and scales up toward 16 as demand increases, then scales back down toward 2 when traffic subsides.
The cheaper minimum defines your baseline capacity. There is a spawn delay before new workers can accept connections, so the minimum must be high enough to absorb short bursts without queuing. The operational playbook recommends that the cheaper minimum represent at least 20% of the maximum worker count to leave adequate headroom.
Misconfigured cheaper settings or monitoring that ignores the cheaper subsystem produces two distinct problems: false alerts when workers scale down, and insufficient baseline capacity when traffic spikes before the system can scale up.
How it works
The master process evaluates the cheaper algorithm on a periodic timer. When the algorithm decides more workers are needed, the master forks new worker processes. When it decides fewer are needed, it signals the target worker to exit gracefully. The worker finishes its current request, then terminates.
Once a worker is cheaped, its stats entry changes. The "status" field becomes "cheap", the "pid" field becomes 0, and the worker is no longer accepting connections. The worker slot remains in the workers[] array of the stats JSON. The array length does not change when workers are cheaped. Only the status and pid fields change.
The following diagram shows how workers transition between states under the cheaper subsystem:
flowchart TD
A["Traffic increases"] --> B["Cheaper algo triggers spawn"]
B --> C["Master forks new worker"]
C --> D["Worker: idle, accepting"]
D --> E["Worker: busy"]
E --> D
F["Traffic decreases"] --> G["Cheaper algo triggers cheap"]
G --> H["Worker finishes current request"]
H --> I["Worker: cheap, pid=0"]
I --> BCheaper algorithms
The cheaper subsystem supports multiple algorithms, each with different availability and scaling logic:
| Algorithm | Available in | How it decides to scale |
|---|---|---|
spare (default) | All builds | Maintains spare idle workers above the minimum |
spare2 | 2.1.x development branch only | Improved spare management |
backlog | Linux with TCP sockets only | Scales based on listen queue depth |
busyness | Plugin (cheaper_busyness) | Scales based on worker utilization percentage |
The spare algorithm is the default and requires no special configuration. It keeps a configurable number of spare workers ready beyond the minimum. When idle workers drop below the spare threshold, it spawns more. When excess workers are idle for too long, it cheaps them.
The backlog algorithm is Linux-only and works only with TCP sockets. It uses the kernel’s listen queue depth as the scaling signal. It does not work with UNIX domain sockets.
The busyness algorithm computes worker utilization and scales up when busyness exceeds a threshold, then down when it drops below one. It requires the cheaper_busyness plugin to be compiled and loaded.
The spare2 algorithm is a common source of confusion. In uWSGI 2.0.x (the PyPI release branch), spare2 is not built in. If you configure cheaper-algo spare2 on a 2.0.x build without the backported plugin, uWSGI logs unable to find requested cheaper algorithm, falling back to spare and silently uses spare instead. In the 2.1.x development branch, spare2 is built in. This reversal between major branches catches operators off guard.
Core configuration options
The essential cheaper directives:
cheaper: minimum number of workers. Must be lower thanworkersorprocesses.workersorprocesses: maximum number of workers.cheaper-algo: which algorithm to use (spare,backlog,busyness, etc.).
Where it shows up in production
The false “missing workers” alert is the most common operational issue with the cheaper subsystem. It happens when monitoring checks that the worker count equals the configured --processes value or some fixed expected count. Traffic decreases, cheaper scales down workers, and monitoring fires an alert. Engineers investigate, find nothing wrong, and dismiss the alert. When a real worker-loss event occurs (crash loop, failed reload), the alert is already ignored.
The fix is not to disable cheaper or raise the minimum to match the maximum. The fix is to change what you alert on.
What to alert on instead
Accepting worker count. Count workers where pid > 0 AND status != "cheap" AND accepting == 1. This is the actual serving capacity at any moment. Alert when this drops below the cheaper minimum, not when it drops below the configured maximum.
# Count accepting workers (non-cheaped, alive, accepting)
uwsgi --connect-and-read 127.0.0.1:9191 | \
jq '[.workers[] | select(.pid > 0 and .status != "cheap" and .accepting == 1)] | length'
Worker busy ratio against non-cheaped workers. Divide busy workers by alive (non-cheaped) workers, not by the configured maximum. This gives the true utilization of active capacity.
# Worker busy ratio (against non-cheaped workers only)
uwsgi --connect-and-read 127.0.0.1:9191 | \
jq '([.workers[] | select(.status == "busy")] | length) as $busy |
([.workers[] | select(.pid > 0 and .status != "cheap")] | length) as $alive |
if $alive > 0 then ($busy / $alive * 100) else 0 end'
Zero accepting workers with a running master. If the master is alive but zero workers have pid > 0 AND status != "cheap" AND accepting == 1, the service cannot serve any requests. This is a genuine emergency, distinct from normal cheaper scaling. A failed reload can reach this state , but cheaper scale-down does not because the cheaper minimum always leaves at least the minimum workers alive.
Exporter behavior and the cheap state
If you use a stats exporter, check how it handles the cheap status. Some exporters treat any worker with status != "busy" as idle, setting the busy gauge to 0 for both idle and cheap workers. Cheaped workers become invisible as a distinct state in the exported metrics. The exporter may report a certain number of “idle” workers when some are actually cheaped (pid 0, not running at all).
This does not cause false alerts on its own, but it makes it harder to distinguish “N workers idle and ready” from “N workers cheaped and M workers idle.” If your exporter does not expose a separate cheap metric, compute the accepting-worker count from the raw stats JSON or configure alerting thresholds that account for the ambiguity.
Tradeoffs and when to use it
When cheaper helps
The cheaper subsystem earns its keep when traffic is variable:
- Traffic has clear peak and off-peak periods (business hours vs nights, batch windows).
- Memory is constrained and running the maximum worker count at all times would exceed available RAM.
- You want automatic capacity during unexpected traffic bursts without manual intervention.
If traffic is predictable and steady-state, a fixed worker pool is simpler and avoids the monitoring complexity.
When cheaper causes problems
- Cheaper minimum too low. If the minimum is set well below the traffic floor, the system oscillates between scaling up and down. Workers are spawned, serve a few requests, then are cheaped. This churn wastes CPU on fork operations and can cause brief capacity dips during each spawn cycle. Set the minimum high enough that normal traffic rarely triggers scaling.
- Cheaper killing busy workers. There are reports across multiple uWSGI 2.0.x versions that cheaper can cheap a worker that is actively serving a request, resulting in a 502 to the client (nginx logs “upstream prematurely closed connection”). The
worker-reload-mercytimeout (default 60 seconds) is supposed to prevent this by giving the worker time to finish, but the behavior has been reported as incomplete or regressed in certain configurations. - Algorithm availability surprises. Configuring
spare2orbusynesswithout verifying the algorithm is actually available in your build leads to silent fallback tospare. You may believe you are running a utilization-based algorithm while actually running simple spare management. Verify withuwsgi --cheaper-algos-list(if available in your version) or check startup logs for the fallback message.
When to avoid cheaper
- Very stable traffic with no meaningful difference between peak and off-peak load.
- Services where worker startup time is long (heavy imports, ML model loading). The spawn delay during scale-up may be too slow to absorb bursts before the listen queue fills.
- Small deployments where the memory savings are negligible relative to the monitoring complexity cheaper introduces.
Signals to watch in production
| Signal | Why it matters | Warning sign |
|---|---|---|
Accepting worker count (pid > 0, status != "cheap", accepting == 1) | True serving capacity at any moment | Drops below cheaper minimum, or reaches zero with master alive |
| Worker busy ratio (busy / non-cheaped alive) | Utilization of active capacity | Sustained above 80% indicates the system is near its scaling ceiling |
| Total worker slots (including cheaped) | Sanity check against configuration | If total slots differ from configured maximum, workers may be failing to spawn |
| Cheaper spawn/cheap events | Detecting oscillation | Rapid spawn-cheap cycles indicate minimum is set too low for the traffic floor |
| Respawns during cheaper operation | Distinguishing cheaper exits from crashes | Respawn rate exceeding max-requests expectations may indicate crash, not cheaper scaling |
| Memory per active worker | Whether the minimum leaves enough headroom | Active worker RSS approaching system limits during scale-up suggests the maximum may be too high for available memory |
The headroom rule: maintain at least 20% idle (non-busy, non-cheaped) workers under normal traffic. With the cheaper subsystem, ensure the cheaper minimum is at least 20% of the maximum worker count. For example, with --processes 20, set --cheaper to at least 4.
How Netdata helps
Netdata’s uWSGI collector reads the stats server JSON and exposes per-worker metrics at per-second resolution. For cheaper-aware monitoring:
- Accepting worker count over time: per-second granularity captures scale-up and scale-down events that coarser polling intervals miss.
- Busy ratio against non-cheaped workers: compute utilization against the active worker count, not the configured maximum, to avoid false capacity warnings during legitimate scale-down.
- Respawn rate alongside cheaper activity: cheaper exits happen at the cheaper boundary; crash respawns happen at any worker count and are uncorrelated with traffic level.
- Worker-level status detail: per-worker status, PID, and request counts let you verify which workers are cheaped versus idle versus busy, and confirm that the algorithm you configured is actually running.
- ML anomaly detection on worker count: learns the normal range for your deployment and flags deviations from that learned pattern, including unexpected scale-down events that might indicate a problem rather than normal cheaper behavior.
Related guides
- uWSGI all workers busy: reading the busy ratio before the queue fills
- uWSGI avg_rt is not a real average: why the latency number lies
- uWSGI chain reload: cycling workers one at a time for zero-downtime deploys
- uWSGI connection refused: clients turned away when the backlog overflows
- uWSGI reload thundering herd: capacity drops to zero during a slow restart
- uWSGI harakiri death spiral: workers killed and respawned while throughput collapses
- uWSGI harakiri not configured: stuck workers with no timeout and no recovery
- uWSGI harakiri timeout: setting it against request duration and nginx timeouts
- uWSGI harakiri-verbose: finding the blocked syscall behind a timeout
- uWSGI HARAKIRI ON WORKER: requests killed for exceeding the timeout
- How uWSGI actually works in production: a mental model for operators
- uWSGI listen backlog and net.core.somaxconn: sizing the connection queue






