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

CauseWhat it looks likeFirst thing to check
Normal client behaviorLow, steady write_errors rate with normal response times and no respawn correlationCompare rate against historical baseline
Slow responseswrite_errors rising alongside avg_rt; clients give up before response arrivesCheck downstream dependency latency
Proxy timeout mismatchwrite_errors spike when responses exceed the proxy upstream timeout; proxy closes the connectionCompare nginx uwsgi_read_timeout against your p95 response time
evil-reload-on-rss killswrite_errors spike aligned with respawn_count increases; workers killed via SIGKILL mid-responseCheck if --evil-reload-on-rss is configured
Harakiri mid-responsewrite_errors aligned with harakiri_count increases; worker killed after timeoutCheck 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

  1. 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.

  2. 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.

  3. 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.

  4. 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_timeout is shorter than your p95 response time, nginx closes the upstream socket before uWSGI finishes writing, and uWSGI logs a write error.

  5. 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

SignalWhy it mattersWarning sign
write_errors rate (per-core sum)Primary indicator of client disconnects during responseSustained increase above baseline
read_errors rate (per-core sum)Connection loss during request body readSustained increase above baseline
avg_rt (EMA, milliseconds)Slow responses cause clients to disconnectRising in lockstep with write_errors
harakiri_count (delta)Harakiri kills workers mid-response, generating write_errorsNon-zero rate correlating with write_errors
respawn_count (delta)evil-reload-on-rss kills generate respawns and write_errorsRespawns exceeding harakiri-driven respawns
Worker RSSMemory growth triggers evil-reload-on-rss thresholdsRSS approaching configured threshold
TX bytes per workerTruncated responses produce less TX than expectedTX 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-rss over --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_timeout whenever you deploy endpoints with different latency profiles.
  • Verify --stats-no-cores is 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.