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
- The default worker is
syncand the default--timeoutis 30 seconds (checked withgunicorn.config.Config().timeout). A sync worker that is busy with one request for longer than that is killed by the arbiter:[CRITICAL] WORKER TIMEOUT (pid:...). - The traceback ends in
handle_abort/SystemExit: 1. Its middle frame is your code (app.py, line 5): that is where the request was stuck. Read it to find the slow call. - The client got
HTTP 500after the timeout (3.7s with--timeout 3), and the arbiter booted a replacement worker. Behind nginx this usually shows up as a 502.
3. Fixes
- Raise the timeout if the slow request is legitimate:
--timeout 10let the 5-second request finish. The same setting ingunicorn.conf.py(timeout = 2in the third trial) is picked up from the working directory without any flag. - Use threads: with
-k gthread --threads 4and--timeout 3, the 5-second request completed with no WORKER TIMEOUT in this test. - Fix the slow call (the frame in your code in the traceback): add a timeout to outbound HTTP/DB calls, or move long jobs to a task queue.
- If workers time out at startup because the app loads a large model, load it lazily, or raise
--timeout.
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.