Deploying a Single WSGI App with uWSGI Emperor: Autoscaling, Reloads, and Safety Checks
Run one WSGI app under uWSGI Emperor with cheaper autoscaling, lazy-apps, harakiri, and Nginx. Includes vassal configuration, load testing, and recovery steps.
23 Jan 2026, 23:30 UTC

The problem with single-app process management
Running one WSGI application in production often forces a choice between a full container orchestrator and a hand-rolled shell script. uWSGI's Emperor mode offers a middle path: a single supervisor process watches a directory of vassal configuration files and starts an isolated uWSGI master for each one. Your app gets its own socket, worker pool, user identity, and lifecycle without sharing a master process with unrelated services.
Desired outcome
By the end of this guide you will have one WSGI app served by an Emperor-managed vassal, fronted by Nginx over a Unix socket, with worker autoscaling, a request watchdog, a stats endpoint, and a safe reload path. The supervisor handles worker respawns; you handle configuration and verification.
Prerequisites
- uWSGI installed on the application host, with the language plugin your app requires.
- A WSGI callable, for example an
appobject importable fromwsgi.py. - Nginx installed and able to proxy to a local Unix socket.
- Root or sudo access to create
/etc/uwsgi/vassalsand/run/uwsgi. - A load-generation tool such as
wrkon a machine that can reach the Nginx listener.
Create the vassal configuration
Each .ini file in the vassals directory becomes one managed application. Create /etc/uwsgi/vassals/app.ini with the following content:
[uwsgi]
master = true
processes = 4
cheaper = 2
cheaper-initial = 2
cheaper-algo = backlog
socket = /run/uwsgi/%n.sock
chmod-socket = 660
uid = www-data
gid = www-data
vacuum = true
die-on-term = true
need-app = true
lazy-apps = true
harakiri = 30
stats = /run/uwsgi/%n-stats.sock
touch-reload = /run/uwsgi/%n.touch
The %n placeholder expands to the filename without its extension, so this file produces /run/uwsgi/app.sock and /run/uwsgi/app-stats.sock. master=true enables the master process that owns the workers. die-on-term=true makes a SIGTERM shut the vassal down cleanly instead of triggering a reload, which matters when a service manager or container stop path sends that signal. need-app=true causes the vassal to exit if the application cannot be imported, preventing silent worker crashes.
Run the Emperor
Create /etc/uwsgi/emperor.ini:
[uwsgi]
emperor = /etc/uwsgi/vassals
Start it in the foreground to observe behavior before wrapping it in a service unit:
sudo uwsgi --ini /etc/uwsgi/emperor.ini
Run this on the application host. You need root or equivalent privileges because the vassals drop to www-data and bind sockets under /run/uwsgi. The expected check is that the Emperor log reports a new vassal master for app.ini and that /run/uwsgi/app.sock appears. If the socket does not appear, confirm that /run/uwsgi exists and is writable by the Emperor process.
Running the Emperor as root is convenient but risky. If an attacker can write to /etc/uwsgi/vassals, they can inject arbitrary configuration that executes as root before privilege drop. Restrict that directory to root ownership and mode 0755, or run a dedicated Emperor user per application when isolation matters more than convenience.
Front the socket with Nginx
Add a location block to your Nginx server configuration:
location / {
include uwsgi_params;
uwsgi_pass unix:/run/uwsgi/app.sock;
}
Reload Nginx after editing. The socket is created with mode 660 and owned by www-data, so the Nginx worker process can connect while other local users cannot. Keep the socket directory writable only by the Emperor and the application group.
Understand the autoscaler and memory tradeoffs
The cheaper subsystem is uWSGI's built-in autoscaler. It maintains at least cheaper workers and may grow toward processes under load. With cheaper-algo=backlog, growth is driven by the Unix-socket or TCP listen backlog: when connections queue faster than workers accept them, uWSGI spawns more workers. The alternative spare algorithm uses idle worker count instead.
| Algorithm | Trigger | Best fit |
|---|---|---|
backlog | Accept queue pressure | Bursty connection rates where queuing directly hurts latency |
spare | Idle worker count | Workloads where request duration varies and idle capacity is the signal |
Choose backlog only when accept-queue pressure matches your latency goal. It does not react to slow application logic inside a request.
lazy-apps=true loads the application once per worker after fork rather than once in the master. This avoids fork-unsafe state such as inherited threads or shared database connections created during import. The cost is higher memory usage and slower worker startup, because every worker imports the application independently. Keep processes bounded and consider reload-on-rss or reload-on-as to cap leaks if you observe growth over time.
Set harakiri as a safety valve, not a strategy
harakiri=30 kills any worker busy longer than 30 seconds and respawns it. Treat this as a safety valve for stuck requests, not a substitute for fixing slow queries or deadlocks. Add harakiri-verbose to the vassal ini if you want explicit log lines when the watchdog fires. Because in-flight work is abandoned, your request handlers must be idempotent. Alert on harakiri events instead of treating respawns as normal background noise.
Verify the deployment
Use these checks to confirm the stack behaves as intended.
1. Emperor and socket status
With the Emperor running in the foreground, confirm each vassal master appears in the log and its socket exists under /run/uwsgi. If a vassal disappears, check the Emperor log for import errors or configuration syntax problems.
2. Worker identity and stats
Query the stats socket from the application host as root or the socket owner:
uwsgi --connect-and-read /run/uwsgi/app-stats.sock
Inspect the returned JSON for worker PIDs and statuses. Confirm privilege drop separately:
ps -o user,pid,cmd -C uwsgi
Expected check: worker processes run as www-data, not root.
3. Controlled load and autoscaling
Drive traffic through Nginx from your load-test host:
wrk -t4 -c100 -d30s http://127.0.0.1/health
Replace 127.0.0.1 with the Nginx address if you are testing remotely. Use a health endpoint that reflects real work rather than a trivial static response. While the test runs, re-read the stats socket and confirm the worker count rises toward processes and later settles back to cheaper. It should never drop below cheaper.
4. Graceful reload
Touch the reload file to reload workers without dropping the socket:
touch /run/uwsgi/app.touch
Re-read the stats socket and confirm worker PIDs have changed. For a full graceful reload of the vassal, send SIGHUP to the vassal master process. Find the PID with ps or the stats output.
5. Harakiri behavior
Temporarily add a route that sleeps past the harakiri limit. Confirm a harakiri log line appears and that the stats socket shows a respawned worker. Remove the route and reload. This validates the watchdog without leaving the slow path in production.
Recovery options
- Vassal will not start: Check the Emperor log for Python import errors or missing directories. Remove the vassal file to stop the app, fix the configuration, and restore the file to restart.
- Socket missing or permission denied: Verify
/run/uwsgiexists, is writable by the Emperor, and thatchmod-socketmatches the Nginx worker user. - Memory climbs after deploy: Lower
processes, disablelazy-appsif your application is fork-safe, or addreload-on-rsswith a measured threshold. - Harakiri fires constantly: Raise the limit only after profiling the endpoint. Frequent kills usually indicate a slow dependency, not a uWSGI problem.
- Shutdown hangs: If
die-on-term=truecauses issues with your service manager stop timeout, verify the unit or container expects a clean exit rather than a brutal reload.
Limitations to keep in mind
The backlog cheaper algorithm reacts to connection queuing, not to slow application logic. lazy-apps multiplies memory across workers. harakiri abandons non-idempotent side effects. And an Emperor running as root is only as safe as the permissions on its vassals directory. Validate with realistic concurrency and a health endpoint that exercises the same code paths as production traffic.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.