Optimizing Python Memory with uWSGI Cheaper Subsystem
Learn how to use the uWSGI Cheaper subsystem to dynamically scale Python worker processes, reducing memory overhead without sacrificing performance during traffic spikes.
13 Aug 2025, 21:53 UTC

The Cost of Idle Workers
Running a Python web application usually involves a trade-off between responsiveness and resource consumption. If you configure a fixed number of uWSGI workers (e.g., 16 workers), those processes occupy system RAM regardless of whether they are handling 1,000 requests per second or zero. On shared hosting or small VPS instances, this "idle tax" often leads to unnecessary memory pressure or forces you to pay for larger instances than your average load requires.
The Cheaper subsystem solves this by transforming uWSGI from a static process pool into a dynamic one. Instead of a fixed count, you define a range, allowing the master process to spawn workers during traffic spikes and reap them during lulls.
Choosing a Scaling Algorithm
Not all scaling is created equal. uWSGI provides several algorithms to decide when to scale, but two are most common for production environments:
- Load-Average: Scales based on the system load average. This is a blunt instrument and can be misleading if other processes on the server are consuming CPU.
- Busyness: The most precise method. It monitors the actual percentage of time workers spend processing requests versus sitting idle. If the average busyness exceeds a defined threshold, uWSGI spawns more workers.
Implementing Busyness-Based Scaling
To implement dynamic scaling, you must move away from a single workers (or processes) directive and instead define a minimum and maximum bound. The following configuration demonstrates a setup for a memory-constrained environment using the cheaper-busyness algorithm.
# uwsgi.ini
[uwsgi]
module = main:app
master = true
# The maximum number of workers allowed
workers = 10
# The minimum number of workers to keep alive (idle)
cheaper = 2
# How many workers to spawn at once when load increases
cheaper-step = 2
# Use the busyness algorithm
cheaper-algo = busyness
# Scale up if workers are busy more than 70% of the time
cheaper-busyness-max = 70
# Scale down if workers are busy less than 30% of the time
cheaper-busyness-min = 30
# Interval to check busyness (seconds)
cheaper-busyness-interval = 5
Execution and Permissions
Run this configuration using the uWSGI binary with the --ini flag. Ensure the user running the process has permissions to manage child processes and write to the log file. If running via systemd, ensure LimitNOFILE is set high enough to accommodate the maximum worker count.
The Trade-off: Latency vs. Efficiency
Dynamic scaling is not a "free lunch." The primary risk is spawn latency. When a sudden burst of traffic arrives and the current workers are saturated, the master process must fork new workers. For heavy Python applications with large imports or complex initialization, this fork can take several hundred milliseconds, causing a momentary spike in request latency for the users who trigger the scale-up.
Another risk is flapping. This occurs when cheaper-busyness-max and cheaper-busyness-min are set too close together. The system may enter a loop of spawning a worker, seeing the average busyness drop immediately, killing the worker, and then repeating the cycle as soon as the next request arrives.
Verifying the Dynamic Pool
To verify that the subsystem is working, you should not rely on the application logs alone, but rather the uWSGI master log and system process monitors.
- Monitor Logs: Look for entries stating
spawnedandkilled. If you see these occurring every few seconds, your thresholds are likely too tight (flapping). - Process Tracking: Run
htoportop. While simulating load (using a tool likewrkorab), observe the number of Python processes. They should climb toward yourworkerslimit and slowly recede toward yourcheaperlimit after the load stops.
Rollback Strategy
If you observe instability or unacceptable latency spikes, revert to a static pool by removing all cheaper- directives and setting workers to your desired constant number. This eliminates the fork/kill overhead at the cost of higher baseline memory usage.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.