Symptom
An incident report correlates worker kills with slow upstream fetches to one suspicious URL and concludes that URL caused the wedge. Typical shape: two slow_ssr_fetch events at 15,051 ms and 15,058 ms to a malformed path, next to two [CRITICAL] WORKER TIMEOUT lines, at the same minute.
Why the correlation is worthless as evidence
A duration that equals your own budget is not a measurement of the server. Look for the arithmetic in your own code before trusting the number:
- SSR load fetch budget:
AbortSignal.timeout(10_000) - one retry on
TimeoutErrorwith a freshAbortSignal.timeout(5_000) - total elapsed logged by the
handleFetchfinallyblock: 10,000 + 5,000 + overhead = 15,05x ms
Every clamped entry in the tail will read ~15,05x ms regardless of what the backend was doing, so a cluster of them says only "the client gave up twice", never "this request was slow". The suspicious URL is in the list because it happened to be in flight, and slow-request logs are biased toward whatever was in flight during an outage.
Three cheap falsifications, in order of strength
- Check the siblings. SSR loads fan out via
Promise.all. If three fetches leave at the same millisecond and only two are in the slow log, the third one's access-log line is a direct measurement of the same code path at the same instant. In one caseGET /v/{user}/viewable/nullwas served 404 in normal time at 01:02:55.226Z while its two siblings burned 15 s — same handler family, same nonexistent key, so the key was not the problem. - Replay against staging. Same commit, no load. If the "hanging" request returns in ~140 ms there, the request is exonerated.
- Find the queued valid request — the strongest signal. Search the access log for a request to a known-good URL that completed within a second of the worker respawn. Its duration is queue time, and
completion_time − durationbounds when the event loop stopped serving. Concretely: a valid view arrived ~01:02:55 and was served at01:03:22.427Z, one line afterWorker (pid:2080) was sent code 134!— 27.0 s of pure queueing, matching a 27,207 ms entry in the same slow log. That proves the instance was already wedged before the suspicious request arrived, which no amount of correlation could have shown.
Why this matters beyond the one incident
An async worker (uvicorn under gunicorn) only trips WORKER TIMEOUT when the event loop misses its heartbeat, and gunicorn's arbiter checks on a timeout/2 period, so detection lags the block by T..1.5T. Sync (def) FastAPI handlers run in the anyio threadpool and cannot block the loop directly — a saturated threadpool queues requests while heartbeats keep flowing. So WORKER TIMEOUT plus healthchecks still returning 200 points at loop starvation (a long GIL-holding CPU section, or a blocking call on the loop), never at "a DB lookup for a missing row was slow".
Rule
Before writing "X correlates with the outage, X caused it", compute whether X's duration is one of your own timeout constants. If it is, X is downstream of the failure. Timestamps of successful requests are better evidence than durations of failed ones.