A 504 from nginx with a uwsgi_pass upstream means nginx stopped waiting for uWSGI before the worker finished. The default uwsgi_read_timeout is 60 seconds. If uWSGI has no harakiri configured (the default), or if harakiri is set higher than uwsgi_read_timeout, the worker keeps processing a request whose response will never be read. The client already has their 504. The worker burns CPU, holds database connections, and occupies a slot that could serve real traffic.
The fix: make one timeout authoritative. Harakiri should fire before uwsgi_read_timeout so uWSGI controls the kill, cleans up the worker, and produces an observable signal. The same ordering principle applies through every proxy layer.
uwsgi_read_timeout is a per-read timeout, not a total response timeout. It resets between successive read operations, so a slow but steadily streaming response will not trigger it. The 504 fires when the worker has not sent the first byte of the response, or has stopped sending for the full timeout window. The entire request processing phase, including all downstream calls, must produce at least one response byte before the timeout expires.
What this means
flowchart TD
A["Request arrives at nginx"] --> B["nginx forwards to uWSGI worker"]
B --> C["Worker begins processing"]
C --> D["Request takes too long"]
D --> E{"Which timeout fires first?"}
E -->|"uwsgi_read_timeout wins"| F["nginx returns 504 to client"]
F --> G["Worker still processing (wasted work)"]
G --> H["Worker writes response, gets SIGPIPE"]
E -->|"harakiri wins"| I["Master kills worker (SIGKILL or SIGTERM)"]
I --> J["nginx sees disconnect, returns 502"]
J --> K["Worker respawned, harakiri_count++"]
E -->|"Neither configured"| L["Worker stuck indefinitely"]
L --> M["Capacity silently degrades to zero"]Three outcomes when a request takes too long:
nginx fires first (504 + wasted work). uwsgi_read_timeout expires. nginx returns 504 to the client and closes the upstream connection. The uWSGI worker does not know the client is gone. It continues processing, eventually writes the response to a closed socket, and receives SIGPIPE.
harakiri fires first (502 + clean cleanup). The master process kills the worker with SIGKILL (or SIGTERM first if harakiri-graceful-timeout is configured) and respawns it. nginx sees the upstream connection drop and returns 502 Bad Gateway. The harakiri_count increments, giving you an observable signal.
Neither fires (silent capacity loss). If harakiri is not configured and the request runs longer than uwsgi_read_timeout, the worker continues until the application code returns. If the application is stuck on a blocking call with no timeout, the worker is consumed permanently. No harakiri_count increment, no alert, just one fewer worker.
The operational principle: harakiri should fire before uwsgi_read_timeout. When it does, uWSGI controls the timeout, the worker is cleaned up, and you get a signal to alert on. When nginx fires first, the worker does wasted work and the only evidence is a SIGPIPE log entry and elevated write_errors in the stats server.
Common causes
| Cause | What it looks like | First thing to check |
|---|---|---|
| harakiri not configured | 504s from nginx, no harakiri log entries, harakiri_count stuck at 0 | grep harakiri in uWSGI config |
| harakiri set higher than uwsgi_read_timeout | 504s from nginx, SIGPIPE in uWSGI log, write_errors climbing | Compare the two configured values |
| proxy_read_timeout used instead of uwsgi_read_timeout | Timeout changes to nginx config have no effect | Verify the location block uses uwsgi_pass with matching directives |
| Downstream dependency slowdown | avg_rt climbing, all workers busy on diverse endpoints | Check database or external API latency |
| post-buffering interaction | harakiri fires much later than expected with non-empty request bodies | Check if post-buffering is enabled |
Quick checks
These commands assume the uWSGI stats server is enabled (stats 127.0.0.1:9191 in your uWSGI config). Adjust the address to match your deployment.
# Check nginx error log for the uwsgi_read_timeout pattern
grep "upstream timed out" /var/log/nginx/error.log | tail -20
# Check uWSGI log for SIGPIPE (the wasted-work signal)
grep -i "SIGPIPE" /var/log/uwsgi/app.log | tail -20
# Check whether harakiri is configured at all
grep -i harakiri /etc/uwsgi/apps-available/*.ini
# Check harakiri_count across all workers (should be nonzero if harakiri is working)
uwsgi --connect-and-read 127.0.0.1:9191 | jq '[.workers[].harakiri_count] | add'
# Check write_errors (broken pipe from nginx closing the connection first)
uwsgi --connect-and-read 127.0.0.1:9191 | jq '[.workers[].cores[].write_errors] | add'
# Check in-flight request ages (are any workers stuck on long requests?)
uwsgi --connect-and-read 127.0.0.1:9191 | jq --argjson now "$(date +%s)" \
'[.workers[] | select(.pid > 0) | .id as $wid | .cores[] | select(.in_request == 1) | {worker: $wid, core: .id, age_seconds: ($now - .req_info.request_start)}]'
# Check avg_rt per worker (is latency approaching the timeout boundary?)
uwsgi --connect-and-read 127.0.0.1:9191 | jq \
'[.workers[] | select(.pid > 0 and .status != "cheap")] | map({id: .id, avg_rt_ms: (.avg_rt / 1000)})'
How to diagnose it
Confirm which timeout fired first. Search the nginx error log for
upstream timed out (110: Connection timed out) while reading response header from upstream. If present, nginx gave up first. Search the uWSGI log forHARAKIRI ON WORKER. Entries at the same timestamps mean the two are racing.Check whether harakiri is configured. The default is no timeout. If
harakiriis absent from the config, every slow request runs until nginx or the application gives up. This is the most common root cause of 504 cascades with no uWSGI-side signal.Compare the two timeout values. If
uwsgi_read_timeoutis 60s (the nginx default) andharakiriis 60s or higher, you have a race or nginx wins outright. Harakiri should be set belowuwsgi_read_timeout.Look for wasted work. Check
write_errorsin the stats server. These increment when the worker writes to a socket nginx has already closed. A rising rate means workers are processing responses nobody will read.Identify the slow endpoint. Check the
urifield orcores[].varson busy workers. If all stuck workers show the same URI, that endpoint is the bottleneck.Check downstream dependencies. If
avg_rtis climbing across all workers on diverse endpoints, the problem is systemic. Correlate with database connection count, query latency, or external API response time.Verify the master process is running. Without a master process, harakiri uses a SIGALRM-based mode that the application can cancel by calling
alarm(). With a master, it uses shared memory timestamps (reliable mode). If your deployment runs without a master, harakiri may not fire when expected.
Metrics and signals to monitor
| Signal | Why it matters | Warning sign |
|---|---|---|
| harakiri_count (delta) | Confirms harakiri is firing. If always 0, it may not be configured. | Non-zero delta means requests are hitting the uWSGI timeout |
| write_errors (delta) | Detects wasted work. Workers writing to sockets nginx already closed. | Rising rate correlates with nginx firing first |
| avg_rt per worker | Shows latency approaching the timeout boundary. avg_rt is an EMA with factor 0.5, not a cumulative average. | avg_rt approaching the harakiri value means workers are about to be killed |
| Stuck request age | Elapsed time of in-flight requests, derived from in_request and request start time. | Request age near uwsgi_read_timeout means 504 is imminent |
| respawn_count (delta) | Every harakiri causes a respawn. | Respawn rate tracking harakiri rate 1:1 confirms harakiri-driven kills |
| nginx 5xx rate | 504s mean nginx fired first; 502s typically mean harakiri fired first. | Sustained 5xx rate from nginx upstream errors |
Fixes
Set harakiri below uwsgi_read_timeout
Make uWSGI’s harakiri the authoritative timeout by setting it lower than nginx’s uwsgi_read_timeout. Determine the appropriate harakiri value for your application (typically 2-3x your expected maximum legitimate request duration), then set uwsgi_read_timeout slightly above it.
# uWSGI config
harakiri = 55
harakiri-verbose = true
# nginx config
location / {
uwsgi_pass unix:///run/uwsgi/app.sock;
uwsgi_read_timeout 60s;
}
With this ordering, when a request hangs: harakiri fires at 55s, the master kills the worker, nginx sees the disconnect before its own 60s timeout and returns 502 instead of 504. The worker is respawned, harakiri_count increments, and no further work is wasted. The harakiri-verbose directive logs the blocked syscall and wchan , which tells you what the worker was stuck on.
The tradeoff: users see 502 instead of 504. Both are errors, but 502 with clean cleanup and an observable signal is operationally better than 504 with wasted worker capacity and silent SIGPIPE.
Use the right nginx directive
If the location block uses uwsgi_pass, the timeout directives must be uwsgi_read_timeout, uwsgi_send_timeout, and uwsgi_connect_timeout. Setting proxy_read_timeout in a uwsgi_pass location has no effect. This leaves the default 60s uwsgi_read_timeout in place regardless of what the operator intended.
Account for post-buffering
If post-buffering is enabled and the request body is non-empty, the effective harakiri timeout becomes a multiple of the configured value . If you rely on harakiri to bound request time with non-trivial request bodies, verify the effective timeout empirically with harakiri-verbose logging during testing.
Walk the full proxy chain
Each layer between the client and uWSGI (CDN, load balancer, nginx) has its own timeout. The same ordering principle applies: the innermost timeout should fire first. A load balancer with a 30s timeout in front of nginx with a 60s uwsgi_read_timeout and a 55s harakiri means the LB returns 504 at 30s while both nginx and uWSGI are still processing. Walk the chain from outside in and ensure each layer’s timeout is progressively shorter.
gevent and async caveat
Harakiri does not work reliably with gevent or async modes because cooperative scheduling can prevent the timeout from being checked . If you run in async mode, do not rely on harakiri. Use application-level timeouts on downstream calls instead, and set uwsgi_read_timeout as the backstop.
Graceful harakiri (uWSGI 2.0.22+)
Before uWSGI 2.0.22, harakiri used SIGKILL with no graceful shutdown window. Version 2.0.22 added harakiri-graceful-timeout, harakiri-graceful-signal (default SIGTERM), and harakiri-queue-threshold. If your uWSGI version supports it, configure graceful harakiri so the worker gets a chance to clean up resources (close database connections, release locks) before being killed.
Prevention
- Always configure harakiri. Without it, a single stuck request permanently consumes a worker with no signal.
- Order timeouts inside-out. harakiri <
uwsgi_read_timeout< LB timeout < CDN timeout. Each layer gives the inner layer a chance to handle the timeout first. - Monitor write_errors. Rising write_errors mean workers are processing responses to closed connections. This is the signature of nginx or an LB firing before uWSGI.
- Monitor harakiri_count. A non-zero delta confirms harakiri is configured and working. A permanently zero count may mean harakiri is not configured, not that nothing is timing out.
- Enable harakiri-verbose. It logs the blocked syscall and wchan when harakiri fires, identifying what the worker was stuck on without a debugger attach.
How Netdata helps
- Correlate nginx 5xx rates with uWSGI harakiri_count and write_errors. If nginx 504s spike while
harakiri_countstays flat, nginx is winning the race and workers are doing wasted work. Ifharakiri_countspikes alongside nginx 502s, the ordering is correct and uWSGI is handling the timeout. - Per-second worker busy ratio and avg_rt. Spot the latency ramp before it hits the timeout boundary. avg_rt approaching the harakiri value is an early warning.
- Write_errors as a wasted-work signal. Netdata surfaces per-core
write_errorsfrom the stats server, making it visible when workers are processing dead requests. - Stuck request age detection. By tracking
in_requestand request start time from the stats server, you can see how long in-flight requests have been running. - ML anomaly detection on avg_rt and worker busy ratio. Anomalous latency spikes that precede timeout races can be flagged before they reach the 504 threshold.
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 cache subsystem: hit ratio drops and ‘full’ insert failures
- uWSGI capacity planning: the leading indicators before saturation
- uWSGI chain reload: cycling workers one at a time for zero-downtime deploys
- uWSGI cheaper subsystem: dynamic worker scaling and the false ‘missing workers’ alert
- uWSGI connection pool cascade: downstream latency that stalls every worker
- uWSGI connection refused: clients turned away when the backlog overflows
- uWSGI Emperor healthy but vassal dead: monitoring each instance independently
- uWSGI file descriptor limits: raising ulimit -n and systemd LimitNOFILE
- uWSGI in gevent/async mode: why worker busy ratio stops meaning anything
- uWSGI threaded mode and the GIL: why more threads don’t add CPU parallelism






