uWSGI workers are vanishing one at a time. The master process is alive, the stats server responds, and harakiri_count reads zero across every worker. By every metric you thought mattered, the service looks healthy. Then you notice that half the workers have been in busy status for the last ten minutes without processing a single new request. Requests are timing out at the proxy. The listen queue is filling. The application is effectively down, and nothing in uWSGI is attempting to recover it.

The root cause is the absence of a harakiri configuration. Default uWSGI has no request timeout. The harakiri value defaults to 0, which disables the per-request watchdog entirely. A single hung request permanently consumes a worker slot. There is no kill, no respawn, and no counter increment. Workers accumulate in a stuck state until the pool is exhausted.

The primary signal operators learn to watch, harakiri_count, stays at zero. That zero is not health. It is a blind spot: the counter only increments when harakiri fires, and harakiri never fires when it is not configured.

What this means

The harakiri timer is uWSGI’s per-request watchdog. When configured, the master process tracks how long each worker has spent on its current request. If the request exceeds the configured timeout, the master sends SIGKILL to that worker and forks a replacement. Each kill increments harakiri_count and respawn_count. The worker slot is recycled within seconds, and the service self-heals.

Without harakiri, none of this happens. A worker that calls a database query with no application-level timeout, hits an infinite loop, or blocks on a deadlocked resource stays in that state indefinitely. The worker is still alive from the OS perspective, still appears in the process table, and still reports status: "busy" in the stats server. But it will never accept another request as long as it lives.

The cascade from a single stuck worker to total outage is mechanical:

flowchart TD
    A["Worker accepts request"] --> B["Application hangs"]
    B --> C["Worker stuck in busy"]
    C --> D{"Harakiri configured?"}
    D -->|No| E["No timeout fires"]
    D -->|Yes| F["Master kills worker, respawns"]
    E --> G["Worker slot lost permanently"]
    G --> H["More workers get stuck over time"]
    H --> I["Pool exhausted, service down"]

The harakiri_count metric is useless when harakiri is not configured. It will read zero forever regardless of how many workers are stuck. Monitoring this counter as a health signal without verifying that the configuration is armed creates false confidence. For a deeper treatment of the worker lifecycle and how stuck workers fit into the broader failure pattern catalogue, see How uWSGI actually works in production.

Common causes

CauseWhat it looks likeFirst thing to check
Blocking database queryOne or more workers stuck on a single URI; database shows long-running queries or lock waitsDownstream database lock and slow query state
External API call without timeoutWorkers stuck after a downstream call; no exceptions loggedDownstream service health and client-side timeout settings
Infinite loop or regex backtrackingWorker CPU pinned at 100 percent; request count frozen/proc/<pid>/syscall and /proc/<pid>/wchan for the stuck PID
DNS resolution hangMultiple workers stuck simultaneously, often after DNS changesResolver configuration and test name resolution directly
Deadlock on shared resourceWorkers stuck with low CPU usage; often follows a recent deployApplication-level lock state and database lock tables

Quick checks

These commands assume a stats server enabled with --stats on a TCP socket at 127.0.0.1:9191. Adjust the address for your deployment. If the stats server uses a UNIX socket, substitute uwsgi --connect-and-read /path/to/stats.sock.

# Check whether harakiri is configured (path varies by deployment)
# Note: this only catches config files, not CLI flags or environment variables.
grep -ri harakiri /etc/uwsgi/

# Count workers by status (busy vs idle)
uwsgi --connect-and-read 127.0.0.1:9191 | jq '[.workers[] | select(.pid > 0 and .status != "cheap") | .status] | group_by(.) | map({status: .[0], count: length})'

# Check harakiri_count across all workers (will be zero if harakiri is not configured)
uwsgi --connect-and-read 127.0.0.1:9191 | jq '[.workers[].harakiri_count] | add'

# Detect stuck requests: elapsed time of in-flight requests per worker
<!-- TODO: verify the exact JSON field path for request start time in the stats output. -->
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)}]'

# Show what each busy worker is serving
uwsgi --connect-and-read 127.0.0.1:9191 | jq '.workers[] | select(.status == "busy") | {id: .id, uri: (.uri // "unknown")}'

# Check total request throughput (sum across workers, compare between polls)
uwsgi --connect-and-read 127.0.0.1:9191 | jq '[.workers[].requests] | add'

# Check what a stuck worker is blocked on (Linux, replace PID)
cat /proc/<pid>/syscall
cat /proc/<pid>/wchan

# Check listen queue depth (Recv-Q on a LISTEN socket shows current accept queue length)
ss -ltn 'sport = :8000'

The cores[] array is suppressed if uWSGI is started with --stats-no-cores. If your stats output does not include cores, you cannot detect stuck request ages from the stats server alone. In that case, rely on per-worker request count deltas and OS-level process inspection.

How to diagnose it

  1. Verify the configuration. Check the uWSGI config file for a harakiri directive. If it is absent or set to 0, the watchdog is disabled. This is the root cause of the missing recovery.
  2. Count accepting workers. Use the stats server to count workers where pid > 0 and status != "cheap" and accepting == 1. Compare against the expected minimum. Each stuck worker is one fewer accepting worker.
  3. Identify stuck workers. Look for workers in busy status whose request count has not changed between two consecutive polls. A worker whose requests counter is frozen while its status is busy is stuck.
  4. Check request age. If cores are available, compute the elapsed time of each in-flight request using req_info.request_start. Any request older than a few seconds for a typical web endpoint is suspect.
  5. Inspect the blocked state. For each stuck worker PID, read /proc/<pid>/syscall and /proc/<pid>/wchan. A worker blocked on read or poll is likely waiting on a downstream dependency. A worker showing no syscall (CPU-bound) may be in an infinite loop.
  6. Check downstream dependencies. The application code is the proximate cause, but the root cause is usually downstream: database locks, dead external APIs, DNS failures, or network partitions.

For the broader pattern of worker pool exhaustion and how it relates to the listen queue, see uWSGI worker pool starvation: the silent outage where every worker is busy.

Metrics and signals to monitor

SignalWhy it mattersWarning sign
harakiri_count (delta)Detects requests exceeding the timeout ceilingNonzero delta means hung requests are being killed; flat zero means either health or misconfiguration
Worker busy ratioShows pool saturation before exhaustionSustained increase with declining throughput indicates stuck workers
Stuck request age (req_info.request_start)The only direct signal for hung requests when harakiri is not configuredRequest age exceeding expected duration without completion
Accepting worker countTracks effective serving capacityDeclining count with stable traffic means workers are being lost to hangs
Request throughput (delta)Confirms real user impactThroughput dropping while incoming traffic is stable means workers cannot complete requests
Respawn count (delta)Distinguishes recycling from stuck workersZero respawn delta with rising busy ratio means no recovery mechanism is firing

Fixes

Configure harakiri

Set harakiri to approximately 2 to 3 times your maximum legitimate request duration. If your slowest normal endpoint takes 10 seconds, set harakiri = 30. This gives legitimate requests headroom while ensuring stuck workers are killed and replaced.

Always enable harakiri-verbose alongside it. When harakiri fires, this flag causes uWSGI to log the blocked syscall and wait channel by reading /proc/<pid>/syscall and /proc/<pid>/wchan (Linux only). Without verbose logging, a harakiri event produces a kill with no diagnostic context.

[uwsgi]
harakiri = 30
harakiri-verbose = true

The shortcut flag -t also sets the harakiri timeout.

The reliable harakiri mode requires master = true. Without the master process, harakiri falls back to a raw SIGALRM-based timer that the uWSGI documentation describes as unreliable. Most production deployments already run with a master, but verify it.

Consider graceful harakiri on uWSGI 2.0.22+

Standard harakiri sends SIGKILL immediately. The worker has no chance to flush buffers, which breaks tracing libraries (Sentry, DataDog, OpenTelemetry) that need to emit span data on exit. uWSGI 2.0.22 introduced options that add a two-stage kill:

  • harakiri-graceful-timeout: gives the worker a grace period to shut down before SIGKILL.
  • harakiri-graceful-signal: the signal sent first (default SIGTERM).
  • harakiri-queue-threshold: only triggers harakiri when the listen queue crosses a threshold, avoiding false kills during brief spikes.

These options are additive and backwards-compatible. If you do not set them, harakiri behaves exactly as before: immediate SIGKILL, no questions asked.

Add application-level timeouts

Harakiri is a last-resort safety net, not a substitute for proper timeout handling in application code. Every downstream call (database queries, HTTP requests, cache lookups) should have its own timeout configured at the library or framework level. This prevents requests from hanging in the first place and reduces the frequency of harakiri-triggered respawns.

Understand async mode limitations

Harakiri is per-process, not per-coroutine. In gevent or asyncio mode, the harakiri counter resets every time a new request kicks in within the same worker. This means harakiri may not fire for an individual slow coroutine if other requests are being multiplexed in the same process. It detects when the entire process is stuck, not when one greenlet is slow. If you run async workers, you cannot rely on harakiri alone for per-request timeout enforcement.

Do not confuse http-timeout with harakiri

http-timeout controls the client-side connection timeout. It does not set a per-request processing timeout. Setting http-timeout without harakiri allows the server to continue processing a request after the client has disconnected, wasting a worker on work no one will see. Both mechanisms are needed.

Prevention

  • Always configure harakiri. Treat it as a mandatory production setting. The default of 0 disables the watchdog.
  • Set harakiri-verbose. Without it, harakiri events are opaque kills with no diagnostic trail.
  • Monitor stuck request age as a compensating signal. When harakiri is configured, harakiri_count detects kills. Request age detection via req_info.request_start catches requests that are slow but have not yet hit the timeout, giving earlier warning.
  • Audit configurations during deployment reviews. Check for the presence of harakiri in every uWSGI config, including Emperor/Vassal configs where individual vassals may override parent settings.
  • Verify master mode. Harakiri’s reliable mode depends on master = true.
  • Set per-call timeouts in application code. Every database query, HTTP call, and external dependency interaction should have a timeout.

For a structured audit of which signals your monitoring stack should be tracking, see uWSGI monitoring checklist: the signals every production app server needs.

How Netdata helps

  • Per-second worker status tracking catches the transition from idle to stuck within seconds, rather than waiting for a coarse polling interval to reveal that a worker has not changed state.
  • Stuck request age detection correlates in_request flags with req_info.request_start timestamps, surfacing individual hung requests even before harakiri would fire.
  • Busy ratio trends show the gradual capacity erosion that precedes pool exhaustion.
  • Throughput correlation confirms whether a busy ratio increase is causing real user impact (throughput dropping) or is a transient spike that self-resolves.
  • Harakiri count as a configuration signal distinguishes between “harakiri_count is zero because the system is healthy” and “harakiri_count is zero because the watchdog is not armed,” by correlating the counter with the known configuration state.
  • Downstream dependency metrics displayed alongside uWSGI worker state shortens root-cause analysis by showing whether database latency, external API response times, or DNS resolution are driving the worker stalls.