write_errors and read_errors are per-core counters in the uWSGI stats server. write_errors increments when a socket write fails during response delivery, almost always because the client disconnected before the response finished (broken pipe). read_errors increments when the connection is lost during request body reading. Some of both are normal: users navigate away, mobile clients switch networks, browsers cancel pending requests. A sustained rise in write_errors, especially when correlated with rising response time or worker respawns, points to a real problem.
The diagnostic work is correlating write_errors with response time, respawn rate, and harakiri count to separate benign disconnects from server-side trouble. Do not treat every spike as an incident, and do not ignore a sustained rise because “clients disconnect all the time.”
What this means
When uWSGI calls write() or writev() to send response bytes to the client and the socket is already closed, the kernel delivers SIGPIPE to the process (or returns EPIPE if SIGPIPE is blocked). uWSGI intercepts this and increments the per-core write_errors counter. The same mechanism produces “write error” or “IOError: write error” in application error trackers like Sentry when the Python layer tries to write to the dead socket.
read_errors follows the same pattern on the input side: the client connection was lost while uWSGI was still reading the request body. This typically happens with slow uploads over unstable networks, proxy timeouts firing during large POST bodies, or clients that crash mid-request.
Both counters live in the stats server JSON under each worker’s cores array:
workers[].cores[].write_errors(per-core, monotonic)workers[].cores[].read_errors(per-core, monotonic)
Both are suppressed entirely if uWSGI is started with --stats-no-cores. If your monitoring shows these metrics as zero or absent, verify that flag is not set before assuming there are no errors. Similarly, if the metrics subsystem is enabled, --metrics-no-cores suppresses per-core metrics from that collection path.
Common causes
| Cause | What it looks like | First thing to check |
|---|---|---|
| Normal client behavior | Low, steady write_errors rate with normal response times and no respawn correlation | Compare rate against historical baseline |
| Slow responses | write_errors rising alongside avg_rt; clients give up before response arrives | Check downstream dependency latency |
| Proxy timeout mismatch | write_errors spike when responses exceed the proxy upstream timeout; proxy closes the connection | Compare nginx uwsgi_read_timeout against your p95 response time |
| evil-reload-on-rss kills | write_errors spike aligned with respawn_count increases; workers killed via SIGKILL mid-response | Check if --evil-reload-on-rss is configured |
| Harakiri mid-response | write_errors aligned with harakiri_count increases; worker killed after timeout | Check which endpoints are timing out |
Quick checks
# Sum write_errors across all workers and cores
uwsgi --connect-and-read 127.0.0.1:9191 | jq '[.workers[].cores[].write_errors] | add'
# Sum read_errors across all workers and cores
uwsgi --connect-and-read 127.0.0.1:9191 | jq '[.workers[].cores[].read_errors] | add'
# Per-worker write_errors and read_errors breakdown
uwsgi --connect-and-read 127.0.0.1:9191 | jq '.workers[] | {id: .id, write_errors: ([.cores[].write_errors] | add), read_errors: ([.cores[].read_errors] | add)}'
# Verify cores array is present (not suppressed by --stats-no-cores)
uwsgi --connect-and-read 127.0.0.1:9191 | jq '.workers[0].cores | length'
# Check per-worker avg_rt (reported in milliseconds)
uwsgi --connect-and-read 127.0.0.1:9191 | jq '.workers[] | select(.pid > 0) | {id, avg_rt_ms: .avg_rt}'
# Check respawn and harakiri counts for correlation with write_errors
uwsgi --connect-and-read 127.0.0.1:9191 | jq '[.workers[] | {id, respawn_count, harakiri_count}]'
# Check worker RSS against evil-reload-on-rss threshold
uwsgi --connect-and-read 127.0.0.1:9191 | jq '.workers[] | select(.pid > 0) | {id, rss_mb: (.rss / 1048576)}'
These commands are read-only and safe to run at any time. The stats server does not require HTTP mode; uwsgi --connect-and-read works with the default raw JSON socket. If your stats server uses a UNIX socket, replace the address with the socket path.
How to diagnose it
Establish the baseline. Poll write_errors and read_errors twice, 60 seconds apart, and compute the per-second rate. Some disconnects are always present. The question is whether the rate has changed significantly from your normal baseline.
Correlate with response time. If write_errors are rising, check avg_rt in the same window. Rising write_errors with normal avg_rt suggests client-side behavior. Rising write_errors with rising avg_rt means your responses are slow enough that clients (or proxies) give up before they arrive.
Correlate with respawn rate. If write_errors spikes align with respawn_count increases, workers are being killed mid-response. Subtract harakiri_count from respawn_count: if the difference matches the write_errors spike, check for
--evil-reload-on-rss. If harakiri_count tracks the spike, workers are timing out on slow requests.Check the proxy timeout boundary. If nginx (or another reverse proxy) sits in front of uWSGI, its upstream timeout controls when the proxy abandons the connection. If nginx’s
uwsgi_read_timeoutis shorter than your p95 response time, nginx closes the upstream socket before uWSGI finishes writing, and uWSGI logs a write error.Check log format variables for per-request detail. If you need per-request granularity, add
%(werr),%(rerr), and%(ioerr)to your uWSGI log format. These variables expose write errors, read errors, and their sum for each individual request, letting you see which endpoints are affected. They have been available since uWSGI 1.9.21.
flowchart td
A["write_errors rate rising"] --> B{"avg_rt also rising?"}
B -->|Yes| C["Slow responses: clients or proxy give up"]
B -->|No| D{"respawn_count also rising?"}
D -->|Yes, tracks harakiri| E["Harakiri killing workers mid-response"]
D -->|Yes, exceeds harakiri| F["evil-reload-on-rss killing workers"]
D -->|No| G{"Proxy timeout < p95?"}
G -->|Yes| H["Proxy closing connections early"]
G -->|No| I["Likely normal client disconnects"]Metrics and signals to monitor
| Signal | Why it matters | Warning sign |
|---|---|---|
| write_errors rate (per-core sum) | Primary indicator of client disconnects during response | Sustained increase above baseline |
| read_errors rate (per-core sum) | Connection loss during request body read | Sustained increase above baseline |
| avg_rt (EMA, milliseconds) | Slow responses cause clients to disconnect | Rising in lockstep with write_errors |
| harakiri_count (delta) | Harakiri kills workers mid-response, generating write_errors | Non-zero rate correlating with write_errors |
| respawn_count (delta) | evil-reload-on-rss kills generate respawns and write_errors | Respawns exceeding harakiri-driven respawns |
| Worker RSS | Memory growth triggers evil-reload-on-rss thresholds | RSS approaching configured threshold |
| TX bytes per worker | Truncated responses produce less TX than expected | TX per request ratio dropping |
Fixes
Slow responses causing client disconnects
If avg_rt is rising alongside write_errors, the root cause is upstream. Your application is taking too long, and clients or proxies disconnect before the response arrives. Fix the downstream dependency (database query, external API call, cache miss path) rather than suppressing the symptom. If you cannot reduce response time immediately, increase the proxy upstream timeout to match your acceptable latency ceiling.
Proxy timeout mismatch
Align nginx’s uwsgi_read_timeout with your actual response time distribution. If your p95 is 5 seconds and uwsgi_read_timeout is 60 seconds, there is no mismatch. If your p95 is 30 seconds and the timeout is 10 seconds, every slow request generates a write error on the uWSGI side.
The nginx directive uwsgi_ignore_client_abort on; tells nginx not to tear down the upstream connection when the client disconnects. This prevents uWSGI from seeing a write error, but the tradeoff is that uWSGI continues processing a request whose response will never be delivered. This wastes worker capacity on abandoned requests. Use it only when you understand this cost.
evil-reload-on-rss killing workers mid-response
--evil-reload-on-rss sends SIGKILL to workers that exceed the RSS threshold, with no grace period. If the worker was mid-response, the client sees a truncated or broken response and uWSGI logs a write error. The fix is to replace --evil-reload-on-rss with --reload-on-rss, which triggers a graceful exit: the worker finishes the current request, then exits. If you must keep --evil-reload-on-rss for hard memory limits, monitor write_errors alongside respawn rates and accept that some clients will see broken responses during recycling.
Harakiri killing workers mid-response
When harakiri fires on a worker that has already started writing its response, the SIGKILL truncates the response and increments write_errors. This is expected behavior if the request genuinely exceeded the timeout. The fix is not to silence the write_errors but to address why the request was slow enough to hit harakiri. If the endpoint legitimately needs more time than the global harakiri, use per-route harakiri overrides (setharakiri) for that specific path.
Suppressing noise from normal disconnects
If you have confirmed that write_errors are from normal client behavior and the noise is polluting logs or error trackers, three uWSGI options work together to suppress it:
--ignore-write-errors true: suppresses uWSGI’s own log messages about write and writev errors--ignore-sigpipe true: suppresses SIGPIPE log messages--disable-write-exception true: prevents Python IOError/OSError exceptions from being raised on write failures
All three are needed because they address different layers. --ignore-write-errors and --ignore-sigpipe stop uWSGI’s C-level logging, but the Python application may still receive an IOError when it tries to write to the closed socket. --disable-write-exception prevents that exception from propagating to application code and error trackers.
The --write-errors-tolerance option sets a threshold for allowed write errors per request before uWSGI takes action. Regardless of the mechanism, this option does not substitute for diagnosing the root cause of sustained write_errors.
read_errors
read_errors are typically less actionable than write_errors. They indicate the client connection was lost during request body reading, which is almost always client-side: network instability, client crash, or a proxy closing the connection during a slow upload. A sustained rise in read_errors without a corresponding rise in write_errors may indicate network problems between the proxy and uWSGI, or proxy misconfiguration dropping connections during large request bodies.
Prevention
- Monitor write_errors as a rate, not an absolute count. The counters are monotonic. Alert on sustained rate increase above baseline, not on the raw value.
- Correlate write_errors with avg_rt and respawn_count. A write_errors spike without corresponding latency or respawn changes is likely benign client behavior.
- Prefer
--reload-on-rssover--evil-reload-on-rss. Graceful recycling avoids mid-response kills that generate write_errors. - Align proxy upstream timeouts with your response time distribution. Review nginx
uwsgi_read_timeoutwhenever you deploy endpoints with different latency profiles. - Verify
--stats-no-coresis not set. If it is, you lose all per-core metrics including write_errors and read_errors. - Add
%(werr)and%(rerr)to your log format. Per-request error counts in access logs let you identify which endpoints are most affected by client disconnects.
How Netdata helps
Netdata collects uWSGI stats server output per second and correlates write_errors and read_errors with worker state.
- Per-second rate charts for write_errors and read_errors show the start and duration of spikes without manual polling.
- Correlation views overlay write_errors with avg_rt, respawn_count, and harakiri_count on a single timeline to separate client-driven disconnects from server-side problems.
- Anomaly detection on write_errors rate flags deviations from the learned baseline, useful when normal disconnect volume is high enough to mask new patterns.
- Per-worker charts show whether write_errors are concentrated on one worker or distributed across all workers.
- RSS charts alongside write_errors and respawn data confirm or rule out evil-reload-on-rss as the cause.
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 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 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
- uWSGI reload thundering herd: capacity drops to zero during a slow restart
- uWSGI harakiri death spiral: workers killed and respawned while throughput collapses






