Context: FastAPI behind gunicorn+UvicornWorker on a 2Gi container (Render), SSR frontend fetching over private network via Node undici.
We raised --max-requests 100 -> 5000 to eliminate the ~2% transient 502/504 rate caused by constant worker-recycle churn during crawler traffic. Within 10 hours prod collected 7 oomKilled(2Gi) + 1 exit-137: the aggressive recycling had been the only thing reclaiming per-request memory spikes (heavy simulation endpoints). Each OOM = hard SIGKILL of the whole container = several seconds with nothing listening = ECONNREFUSED bursts, which are WORSE than the recycle churn (a crawl OOM'd both instances simultaneously mid-crawl).
Diagnosis discriminators that generalize:
- Client-side undici error text tells you which failure you have:
connect ECONNREFUSED ip:port= nothing listening (restart gap / OOM kill);read ECONNRESET/other side closed= process alive but connection killed under load (event-loop stall widening the keep-alive close race). - Gunicorn boot-line fingerprint: one-at-a-time 'Booting worker' with growing pids = max-requests recycling; simultaneous low-pid triple boots + 'Starting gunicorn' with NO 'Handling signal: term' = hard container kill (OOM).
- On Render, confirm via the events API:
GET /v1/services/<id>/eventsfilteringtype==server_failedshowsoomKilled {memoryLimit}explicitly.
Lesson: request-count recycling is a memory-reclaim mechanism in disguise. Before raising/removing it, verify per-request spike behavior under your worst burst load (crawlers), or move to memory-threshold recycling (SIGTERM worker on RSS limit; gunicorn/uvicorn have no native knob). A middle value (e.g. 1000) trades ~5x less churn for retained spike reclamation. Also: client-side single retry of idempotent GETs on connection-level errors makes both failure modes invisible to users.