uWSGI Emperor Vassal Reloads: Signal Boundaries and Deploy Safety
Use SIGUSR2 to the uWSGI emperor, not SIGTERM to vassals, for zero-downtime code deploys. Verify with the stats endpoint before and after.
17 Jul 2025, 02:04 UTC

The problem and the useful takeaway
You need to ship a new version of a Python application behind uWSGI without dropping in-flight HTTP requests. Restarting worker processes individually, or sending the wrong signal to the wrong process, can terminate active connections and leave the load balancer sending traffic to a dead socket. The practical rule for uWSGI emperor mode is: signal the master process with SIGUSR2, never SIGTERM a vassal directly, and verify worker state through the stats endpoint before and after the reload.
Requirements
- Zero-downtime code deploys for a WSGI application.
- Worker isolation: one bad vassal must not crash the whole stack.
- Observable health: you must confirm a reload happened without losing active requests.
- Boundaries: untrusted users must not be able to inject vassal configs or read internal stats.
Smallest suitable design
Run one uWSGI emperor as the master process. The emperor scans a vassal directory and starts one vassal per application config. Each vassal runs its own master and workers. The emperor owns the reload signal; vassals own sockets and worker processes.
[uwsgi]
emperor = /etc/uwsgi/vassals
stats = /run/uwsgi/stats.socket
pidfile = /run/uwsgi/emperor.pid
daemonize = /var/log/uwsgi/emperor.log
A vassal config might look like this:
[uwsgi]
chdir = /srv/app
module = wsgi:application
socket = /run/uwsgi/app.sock
chmod-socket = 660
master = true
processes = 4
reload-on-rss = 1024
max-requests = 2000
Terms: an emperor is the uWSGI process that supervises vassals. A vassal is an isolated application instance with its own master and workers. SIGUSR2 is the POSIX signal that tells the emperor to reload vassals gracefully. The stats endpoint is a JSON or XML interface uWSGI exposes over HTTP or a UNIX socket.
Trust and data boundaries
- The vassal directory should be writable only by a trusted deploy user. Any user who can drop a .ini file there can control how uWSGI starts an application.
- Socket files should use a restricted group and mode, such as 660, so only the application group can connect.
- The stats socket should bind to localhost or a UNIX socket, never a public interface. It exposes worker PIDs, request counts, and memory usage.
- If you use cache2 or Redis for shared state, treat it as a trust boundary: vassals must not write overlapping keys, and the cache must be reachable from all vassals.
Operational checks and commands
Run these commands on the application host as root or the uwsgi service user. The user must be able to read the pidfile and signal the emperor process.
Confirm the emperor PID
cat /run/uwsgi/emperor.pid
Trigger a graceful reload
kill -USR2 $(cat /run/uwsgi/emperor.pid)
Expected check: the emperor log, typically /var/log/uwsgi/emperor.log, should show vassals restarting one at a time. Active requests should continue on the old workers until they finish, then the new code loads.
Verify worker state
curl --unix-socket /run/uwsgi/stats.socket http:/stats
Compare the workers array before and after the reload. Worker PIDs should change, while the worker count remains stable and no worker stays in a dead or starting state for more than a few seconds. Do not assume a specific JSON field name; inspect the structure returned by your uWSGI version.
Risks
- Signaling the wrong PID sends the reload to an unrelated process.
- Sending SIGTERM directly to a vassal kills its workers immediately and drops in-flight requests.
- A long-running request can delay the reload because uWSGI waits for it to complete before replacing the worker.
Failure modes
| Failure mode | Cause | Detection |
|---|---|---|
| Reload stalls | An in-flight request never completes, so uWSGI waits to replace the worker. | Emperor log shows reload started but worker PIDs do not change after several minutes. |
| Crash loop | A vassal config error prevents the app from importing; emperor restarts it repeatedly. | Emperor log repeats start and exit messages for the same vassal. |
| Thread-state interruption | Python GIL and thread state mean signals can arrive mid-operation; rare cases can leave a worker in an inconsistent state. | Application logs show exceptions after reload; stats show worker restarts without new deploys. |
| Stale socket | Old worker keeps the UNIX socket after a failed reload; new workers cannot bind. | Connection refused on the application socket; emperor log reports bind errors. |
Conditions that change this design
- If the application uses async modes such as gevent, signal handling and worker recycling behave differently; test reloads in staging first.
- If max-requests is set to a low value, workers recycle constantly on their own, reducing the value of signal-driven reloads.
- If zero-downtime is not required, a rolling restart through systemd or container orchestration is simpler than managing emperor signals.
- If uWSGI is older than 2.0.10, signal handling may differ from later versions; confirm behavior in your release before relying on SIGUSR2.
Rollback
A reload changes worker PIDs and loads new code. To roll back, restore the previous application code or vassal config, then send SIGUSR2 to the emperor again. Confirm through the stats endpoint that worker PIDs have changed back and the error rate has dropped.
Practical verification
Before a deploy, record worker PIDs from the stats endpoint. After sending SIGUSR2, poll the endpoint until all PIDs have changed and every worker reports a running state. If any worker remains missing or the socket stops accepting connections, stop and inspect the emperor log before sending more signals.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.