API 504 Gateway Timeout: diagnose the upstream delay
A 504 does not say that your API key is bad, and it does not prove that a model provider is down. It says a gateway waited for an upstream response and did not receive one in time. Start with a small, repeatable test and keep the request ID and timestamps.
What does HTTP 504 prove?
RFC 9110 defines 504 Gateway Timeout narrowly: a server acting as a gateway or proxy did not receive a timely response from an upstream server. The status code identifies the layer that gave up waiting. It does not identify the slow component.
| Response | What it establishes | What to check next |
|---|---|---|
| 401 JSON | The receiving service rejected authentication | Bearer header, key, account or IP restrictions |
| 404 | The route or requested resource was not found | Base URL, endpoint path and Model ID |
| 429 | A documented rate or quota limit blocked the request | Error code, limit headers and Retry-After when present |
| 504 | A gateway timed out while waiting upstream | Request ID, timing, route, stream state and server logs |
Run the smallest useful test
Check authentication separately before repeating a slow generation. The model-list route is cheap and does not ask an inference model to generate output.
: "${MONKEYAI_API_KEY:?Set MONKEYAI_API_KEY first}"
curl -i https://monkeytheme.com/v1/models \
-H "Authorization: Bearer $MONKEYAI_API_KEY"A 200 JSON model list confirms that this key can reach the model-list route. It does not guarantee that every model or inference route is healthy. A 401 is an authentication failure, not an upstream timeout.
Next, retry the failed workload once with a shorter prompt and a small output limit. Keep the same model and endpoint so you only change one variable. If the small request succeeds, save both timings; that is useful evidence, not proof that request size caused the original 504.
Capture status, headers and timing in one command
This command keeps the response headers and body in separate temporary files. It also prints connection time, time to first byte and total time, which makes a support report much more useful than a screenshot of "504."
: "${MONKEYAI_API_KEY:?Set MONKEYAI_API_KEY first}"
workdir=$(mktemp -d)
curl -sS \
-D "$workdir/headers.txt" \
-o "$workdir/body.txt" \
-w 'status=%{http_code}\nconnect=%{time_connect}s\nfirst_byte=%{time_starttransfer}s\ntotal=%{time_total}s\n' \
https://monkeytheme.com/v1/models \
-H "Authorization: Bearer $MONKEYAI_API_KEY"
printf 'Saved to %s\n' "$workdir"If body.txt contains JSON, preserve its error type and message after removing private data. If it contains an HTML gateway page, record that fact too; an HTML 504 and a structured JSON API error can come from different layers. Do not publish headers.txt without checking it for cookies, trace data or other sensitive values.
Capture evidence before retrying again
| Record | Why support needs it |
|---|---|
| UTC time and duration | Correlates the client failure with proxy and upstream logs |
| Endpoint and Model ID | Identifies the route without exposing the prompt |
| HTTP status and response body | Separates a gateway page from a structured API error |
| Request ID / trace header | Provides the strongest log lookup key when present |
stream setting | Shows whether the timeout happened before the first event or between events |
| One redacted request shape | Shows token/output settings while keeping the API key private |
***. Redact private prompt content as well.Why streaming can still time out
Streaming lets the client process output while generation continues, but it does not remove proxy timeouts. Nginx documents proxy_read_timeout as the allowed gap between successive reads from upstream, not a limit on the complete response. Its documented default is 60 seconds.
If the upstream sends nothing before that gap expires, the proxy can close the connection. Once a stream has already sent data to the client, a proxy cannot safely replay the same response as though nothing happened.
background: true for long-running Responses API tasks. This is not a universal OpenAI-compatible feature.Production-anchored case study (August 7–8, 2026)
The following signals come from a real journalctl -u sub2api --since '24 hours ago' sweep on the gateway host. Numbers are not staged or fabricated; they reflect one stress-test model (gpt-5.6-sol) hitting the same scenario repeatedly. The lesson is not "the gateway is broken" — the lesson is the exact log shape that distinguishes a 502-from-upstream from a 503-from-no-available-accounts.
| Status code | Count in 24h | Path | Latency signal | What the log indicates |
|---|---|---|---|---|
| 502 | 47 | /v1/chat/completions | 170–570 ms | Upstream returned 502; gateway surfaced it (account id 16 attached) |
| 503 | 235 | /v1/chat/completions | 13–25 ms | No account available — fails fast before upstream is even contacted |
| 200 | (rest of 1457) | same | varies | Healthy path |
Three error fingerprints from the same window, all anonymized to internal model gpt-5.6-sol and account id 16:
# (1) Upstream returned 401 → gateway switched accounts (502 surfaced after max_switches exhausted)
... openai_chat_completions.upstream_failover_switching {
"model": "gpt-5.6-sol", "stream": true,
"account_id": 16, "upstream_status": 401,
"switch_count": 1, "max_switches": 10
}
# (2) Final 502 to the client after upstream exhausted switches
... http request completed {
"path": "/v1/chat/completions", "method": "POST",
"status_code": 502, "latency_ms": 570,
"account_id": 16, "platform": "openai", "model": "gpt-5.6-sol"
}
# (3) Faster path: gateway had no available accounts at all → 503 in 13–25 ms
... openai_chat_completions.account_select_failed {
"model": "gpt-5.6-sol", "stream": true,
"error": "no available accounts", "excluded_account_count": 0
}
... http request completed {
"status_code": 503, "latency_ms": 15
}
- latency_ms 13–25 with 503 + "no available accounts" — the gateway did not even reach upstream. This is an operator-side issue (account pool exhausted, all in cooldown, or model unmapped), not a network timeout. Bumping client-side retries does not help.
- latency_ms 170–570 with 502 +
upstream_failover_switching— upstream did respond, but with a bad status. The gateway attempted up tomax_switchesaccount hops before surfacing 502. This is the classic "healthy proxy, sick upstream pool" pattern. - Neither log line corresponds to a literal Nginx
504. If a 504 actually appears in your client logs, the proxy itself (the layer in front of this gateway) gave up waiting — i.e. a different timer fired. Confirm by checking the proxy'saccess.logfor the matching upstream status and latency; the layer that emits 504 is the one with no further retries to attempt.
504 entries; only 502/503 from the Go gateway. A literal client-side 504 still implies a layer above this gateway — typically a CDN/edge proxy with its own read timeout. The diagnostic recipe in this guide applies equally, but the "which timer fired" answer will point at a different layer.Retry without multiplying work
- If the response includes
Retry-After, honor it. - Otherwise wait briefly, then use bounded exponential backoff with jitter.
- Retry a failed generation only when duplicate work is acceptable or you have an idempotency strategy.
- Stop after a small number of attempts and send the evidence above to support.
OpenAI’s public error guide documents retry advice for temporary 503 conditions and its rate-limit guide covers Retry-After and backoff. It does not guarantee that a 504 response includes Retry-After.
max_retries=0 temporarily if you need to observe one attempt; restore an intentional retry policy afterward.Operator checks
If you operate the relay, correlate the client timestamp with reverse-proxy and application logs. Check the upstream address, connect time, time to first byte, response status, and whether any bytes reached the client.
| Nginx setting | Documented behavior |
|---|---|
proxy_connect_timeout | Limits how long Nginx waits to establish the upstream connection; default 60s |
proxy_read_timeout | Limits the gap between successive upstream reads; default 60s |
proxy_send_timeout | Limits the gap between successive writes to upstream; default 60s |
proxy_next_upstream | Does not include an upstream HTTP 504 response in its default retry conditions |
proxy_buffering off, proxy_request_buffering off, and 1800-second read/send timeouts in its Nginx baseline for long-lived SSE and WebSocket requests. Treat that as a comparison point, not a production prescription. Compare it with the route that failed, your CDN limits, and your own latency data before changing anything.Raising every timeout globally is a blunt fix. Confirm which timer fired and which route needs more time before changing production limits.
Sources
FAQ
Does a 504 mean my API key is invalid?
No. Invalid authentication normally returns 401. A 504 means a gateway timed out while waiting for an upstream response.
Does a successful /v1/models request prove chat will work?
No. It confirms the key and Base URL work for the model-list route. It does not guarantee that a specific inference route or model is healthy.
Should I keep retrying a 504?
Retry once after a short wait, then use bounded backoff if duplicate work is safe. Stop and collect request evidence instead of looping indefinitely.
Will stream=true prevent a 504?
No. Streaming can reduce perceived waiting time, but the upstream still has to send data before the proxy read timeout expires.
Should operators raise proxy_read_timeout everywhere?
No. First confirm which timer fired and which route needs more time. A global increase can hide slow or unhealthy upstreams.
Related guides
API 502 Bad Gateway
The opposite case: upstream replied, but with an invalid response. Use latency and log shape to tell 502 from 504.
Open page