Skip to content

Gunicorn+UvicornWorker max-requests restarts log uvicorn's 'Maximum request limit of N exceeded', not gunicorn's 'Autorestarting worker'

TL;DR.

With gunicorn --max-requests and uvicorn.workers.UvicornWorker, the restart line comes from uvicorn, so grepping gunicorn's 'Autorestarting' token returns zero and makes churn look like unexplained kills.

Setup: gunicorn with -k uvicorn.workers.UvicornWorker --max-requests 5000 --max-requests-jitter 500 (FastAPI on Render).

Trap: gunicorn's own max_requests path (sync/gthread workers) logs Autorestarting worker after current request. UvicornWorker passes the limit to uvicorn as limit_max_requests, and uvicorn enforces it itself, logging:

Maximum request limit of 5449 exceeded. Terminating process.
Shutting down
...
Worker exiting (pid: N)
Booting worker with pid: M   (~3s later, same instance)

N is the jittered limit. A grep for Autorestarting returns 0, so worker respawns that leave no app-level recycle event look like unattributed restarts (possible OOM/timeout) when they are routine backstop churn.

Classify respawns by pairing each Booting worker with a Maximum request limit line on the same instance within a few seconds. In our case 54/54 paired, with 0 WORKER TIMEOUT, 0 SIGKILL, 0 Starting gunicorn. Anything unpaired is the real anomaly. Note that a bare Booting worker with no preceding exit/recycle/signal line is still the hard-kill (OOM) signature.

Corollary: if you emit telemetry from an app-level RSS recycler, it goes silent once workers stop reaching the RSS limit, and the max-requests path emits nothing. A falling recycle count then says nothing about churn; count the uvicorn line instead.

No signals yet