gunicorn CRITICAL WORKER TIMEOUT (tested)

[CRITICAL] WORKER TIMEOUT (pid:...) means a gunicorn worker spent longer than --timeout (default 30 seconds) on one request, so the arbiter killed it and the client got a 500. Raise --timeout for legitimately slow requests, switch to -k gthread, or fix the slow call shown in the traceback.

1. Reproduce WORKER TIMEOUT

app.py, a WSGI app that sleeps for ?N seconds:

import time

def app(environ, start_response):
    secs = float(environ.get("QUERY_STRING") or 0)
    time.sleep(secs)                      # stands in for a slow DB call / model load
    start_response("200 OK", [("Content-Type", "text/plain")])
    return [f"slept {secs}s\n".encode()]

run.py starts gunicorn with different settings and sends one slow request each time:

import subprocess, sys, time, urllib.request
G = "/tmp/fx384/ve/bin/gunicorn"
def trial(args, q, full=False):
    cmd = [G, "-b", "127.0.0.1:8965", *args, "app:app"]
    print("$", "gunicorn", " ".join(cmd[1:]))
    p = subprocess.Popen(cmd, stderr=subprocess.PIPE, text=True)
    time.sleep(1.5)
    t = time.time()
    try:
        r = urllib.request.urlopen(f"http://127.0.0.1:8965/?{q}", timeout=60).read().decode().strip()
    except Exception as ex:
        r = f"{type(ex).__name__}: {ex}"
    print(f"$ curl 'http://127.0.0.1:8965/?{q}'  -> {r}  ({time.time()-t:.1f}s)")
    time.sleep(1); p.terminate(); err = p.communicate()[1]
    for line in err.splitlines():
        if "Listening at" in line or "Starting gunicorn" in line or "Handling signal" in line or "Shutting down" in line: continue
        if not full and not line.startswith("["): continue
        print("  " + (line[line.index("] ")+2:] if line.startswith("[") else line))
    print()
trial(["--timeout", "3"], "5", full=True)
trial(["--timeout", "10"], "5")
import os; os.rename("gc.tmp","gunicorn.conf.py")
trial([], "3")  # gunicorn.conf.py in the cwd: timeout = 2
os.rename("gunicorn.conf.py","gc.tmp")
trial(["--timeout", "3", "-k", "gthread", "--threads", "4"], "5")

Output (gunicorn 26.2.0, Python 3.13.5, Linux; first trial with the full log):

$ gunicorn -b 127.0.0.1:8965 --timeout 3 app:app
$ curl 'http://127.0.0.1:8965/?5'  -> HTTPError: HTTP Error 500: Internal Server Error  (3.7s)
  [3394044] [INFO] Using worker: sync
  [3394045] [INFO] Booting worker with pid: 3394045
  [3394044] [INFO] Control socket listening at /run/user/1000/gunicorn.ctl
  [3394044] [CRITICAL] WORKER TIMEOUT (pid:3394045)
  [3394045] [ERROR] Error handling request GET /?5
  Traceback (most recent call last):
    File "/tmp/fx384/ve/lib/python3.13/site-packages/gunicorn/workers/sync.py", line 141, in handle
      self.handle_request(listener, req, client, addr)
      ~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    File "/tmp/fx384/ve/lib/python3.13/site-packages/gunicorn/workers/sync.py", line 184, in handle_request
      respiter = self.wsgi(environ, resp.start_response)
    File "/tmp/gw405/app.py", line 5, in app
      time.sleep(secs)                      # stands in for a slow DB call / model load
      ~~~~~~~~~~^^^^^^
    File "/tmp/fx384/ve/lib/python3.13/site-packages/gunicorn/workers/base.py", line 199, in handle_abort
      sys.exit(1)
      ~~~~~~~~^^^
  SystemExit: 1
  [3394045] [INFO] Worker exiting (pid: 3394045)
  [3394086] [INFO] Booting worker with pid: 3394086
  [3394044] [INFO] Control socket listening at /run/user/1000/gunicorn.ctl
  [3394086] [INFO] Worker exiting (pid: 3394086)

$ gunicorn -b 127.0.0.1:8965 --timeout 10 app:app
$ curl 'http://127.0.0.1:8965/?5'  -> slept 5.0s  (5.0s)
  [3394088] [INFO] Using worker: sync
  [3394089] [INFO] Booting worker with pid: 3394089
  [3394088] [INFO] Control socket listening at /run/user/1000/gunicorn.ctl
  [3394089] [INFO] Worker exiting (pid: 3394089)

$ gunicorn -b 127.0.0.1:8965 app:app
$ curl 'http://127.0.0.1:8965/?3'  -> HTTPError: HTTP Error 500: Internal Server Error  (2.7s)
  [3394307] [INFO] Using worker: sync
  [3394308] [INFO] Booting worker with pid: 3394308
  [3394307] [INFO] Control socket listening at /run/user/1000/gunicorn.ctl
  [3394307] [CRITICAL] WORKER TIMEOUT (pid:3394308)
  [3394308] [ERROR] Error handling request GET /?3
  [3394308] [INFO] Worker exiting (pid: 3394308)
  [3394356] [INFO] Booting worker with pid: 3394356
  [3394307] [INFO] Control socket listening at /run/user/1000/gunicorn.ctl
  [3394356] [INFO] Worker exiting (pid: 3394356)

$ gunicorn -b 127.0.0.1:8965 --timeout 3 -k gthread --threads 4 app:app
$ curl 'http://127.0.0.1:8965/?5'  -> slept 5.0s  (5.0s)
  [3394363] [INFO] Using worker: gthread
  [3394364] [INFO] Booting worker with pid: 3394364
  [3394363] [INFO] Control socket listening at /run/user/1000/gunicorn.ctl
  [3394364] [INFO] Worker exiting (pid: 3394364)

2. What the log means

3. Fixes

Related: where gunicorn logs go (access log off by default).

Frequently asked questions

What is the default gunicorn worker timeout?

30 seconds, with the sync worker class.

How do I increase the gunicorn timeout?

Pass --timeout 120 on the command line, or put timeout = 120 in gunicorn.conf.py in the working directory.

What does the client see on WORKER TIMEOUT?

In this test the client got HTTP 500 right after the timeout; through a reverse proxy it often appears as 502.

Written with AI assistance. Measured on 2026-10-23 with gunicorn 26.2.0 and Python 3.13.5 on Linux. The script ran verbatim and the output was pasted unedited. PlainHeaders is our extension; gunicorn is free.