API 502 Bad Gateway: separate upstream failures from timeouts
A 502 means an upstream replied, but with something your proxy considered invalid. It is the opposite of 504: there is a response, the gateway just cannot pass it through. Confirm what your logs call the failure before retrying, because 502, 503 and 504 need different fixes.
What HTTP 502 actually means
RFC 9110 defines 502 Bad Gateway: a server acting as a gateway or proxy received an invalid response from an upstream server. The status code identifies the proxy layer that surfaced the failure, not the upstream that produced it. An upstream here can be the model provider, a CDN in front of it, an internal service, or the OpenAI-compatible relay in front of the public route.
The article API 504 Gateway Timeout covers the no-reply-in-time case. This page covers the bad-reply case. Both come from the same family of proxy-failure status codes, but they need different diagnostics.
502 vs 504 vs 503: read the latency
You can usually tell them apart with the client-side latency_ms and the response body. The numbers below are drawn from a real journalctl -u sub2api --since '24 hours ago' sweep on the gateway host, anonymized to model gpt-5.6-sol and account id 16. They are not staged.
| Status | Latency band (24h) | Count (24h) | What the latency says |
|---|---|---|---|
| 502 | 110-1232 ms | 47 | The gateway reached upstream and upstream replied, but with something invalid. The gateway gave up after a small number of switches. |
| 503 no available accounts | 13-25 ms | 235 | The gateway never reached upstream. The account pool was empty or all excluded for the requested model. |
| 200 (rest of traffic) | varies | 1457+ | Healthy path on the same model. |
If latency is under 30 ms with a 503, the relay never reached upstream — it is an operator-side account-pool exhaustion, not a network problem. If latency is 100-1500 ms with a 502, the relay did reach upstream and the upstream replied badly. Mixing the two produces the wrong fix.
Run the smallest useful test
Check authentication separately before assuming the upstream is the problem. The model-list route is cheap and does not call an inference model.
: "${MONKEYAI_API_KEY:?Set MONKEYAI_API_KEY first}"
curl -i https://monkeytheme.com/v1/models \
-H "Authorization: Bearer ***"A 200 JSON model list confirms that the key reaches the model-list route. A 401 is an authentication failure, not a 502. A 404 means the route is wrong, not the upstream.
If /v1/models returns 200 but a specific chat call still returns 502, the failure is on the inference path, not on the relay or your credentials. Reduce the test to a short prompt with a small output limit and reuse the same model id so you only change one variable at a time.
Capture status, headers and timing in one command
Keep the response headers and body in separate temporary files. Print connection time, time to first byte and total time. A 502 with body text is much more useful than a 502 with only a status code.
: "${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/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ***" \
-d '{"model":"gpt-5.6","messages":[{"role":"user","content":"hi"}],"max_tokens":16}'
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 502 from a CDN edge and a structured JSON 502 from the relay describe different layers. Do not publish headers.txt without checking it for cookies, trace data or other sensitive values.
What production logs look like for 502
The following log shape comes from the same 24-hour window as the latency table. Every 502 in that window was preceded by an upstream_failover_switching warning with upstream_status of 401 and a single account id (16) in the field. That pattern is what tells you this is an upstream-replies-badly scenario, not a no-reply-in-time scenario.
# (1) Upstream returned 401 to the relay, relay switched accounts
... 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) Relay exhausted its account pool, no fallback survived
... openai_chat_completions.account_select_failed {
"model": "gpt-5.6-sol", "stream": true,
"error": "no available accounts", "excluded_account_count": 1
}
# (3) Final 502 returned to the client after ~448 ms
... http request completed {
"path": "/v1/chat/completions", "method": "POST",
"status_code": 502, "latency_ms": 448,
"account_id": 16, "platform": "openai", "model": "gpt-5.6-sol"
}Three signals distinguish a 502-from-upstream-bad-reply from a 503-from-no-accounts at a glance:
| Signal in the log | What it indicates |
|---|---|
upstream_failover_switching with non-zero switch_count | The relay reached upstream and the reply was bad enough to trigger a switch |
upstream_status field present and not 200 | Upstream replied with a specific HTTP status that the relay considered non-retryable here |
latency_ms in the 100-1500 ms band on the final 502 line | The whole upstream round-trip ran; this is not a no-reply timeout |
Retry without multiplying work
A bounded retry on a 502 is reasonable when the bad upstream is transient and the relay can hop to a healthy account. It is not reasonable when every retry hits the same account or the same upstream path.
- Confirm the response includes no
Retry-Afterheader before you ignore it; if it does, honor it. - Use bounded exponential backoff with jitter; one retry is usually enough to distinguish a real outage from a one-off blip.
- If the response body is identical across two retries, stop — a third retry will reproduce the same failure.
- Send the evidence captured above to support. A redacted
request_idand the model id are usually enough for the relay operator to find the corresponding upstream log line.
max_retries=0 temporarily if you need to observe one attempt; restore an intentional retry policy afterwards.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, the TLS handshake, and whether any bytes reached the client.
| Layer | Signal to look for | Where to look |
|---|---|---|
| Public CDN edge (Cloudflare, etc.) | HTML 502 page or a 5xx with cf-ray and an edge status code in the response headers | Edge access logs or the worker that fronts the public route |
| Reverse proxy (Nginx, Caddy) | upstream prematurely closed connection, 502 Bad Gateway from proxy_intercept_errors | Nginx error.log; check proxy_next_upstream settings |
| Application relay | upstream_failover_switching warning before the 502 line; account_select_failed immediately after | Application structured logs; correlate via request_id |
| Upstream model provider | 5xx, empty body, or TCP reset on the upstream connection | Provider's own status page; their per-account quota view if available |
Raising every timeout globally is a blunt fix. Confirm which layer the 502 came from before changing production limits; a CDN-level 502 will not improve by raising the application read timeout.
Sources
FAQ
Does a 502 mean my API key is invalid?
Not usually. A 401 is the documented authentication failure. A 502 means an upstream returned an invalid or empty response while the gateway had already accepted your request.
How is 502 different from 504?
502 Bad Gateway means the upstream replied, but with something the gateway considers invalid. 504 Gateway Timeout means the upstream did not reply at all within the proxy read timeout. Same family, opposite failure mode.
How is 502 different from 503 "no available accounts"?
A 503 under 30 ms with an account_select_failed line means the relay never reached upstream. A 502 in the 100-1500 ms band with an upstream_failover_switching line means the relay did reach upstream and the upstream replied badly.
Will retrying fix a 502?
Sometimes, when the bad upstream is transient and a different account or route survives. If every retry hits the same account, retrying reproduces the same failure and burns your rate-limit budget.
Should I switch Base URL or model when I see 502?
Switch the model first if you have alternatives. Do not change the Base URL until you have evidence that the public endpoint itself is the layer that returned 502.
Related guides
API 504 Gateway Timeout
The opposite case: upstream did not reply in time. Use latency and log shape to tell them apart.
Open page503 no available accounts
The fastest path: the relay never reached upstream because the account pool was empty.
Open pageChatGPT Plus 5-hour window
When the upstream-side account pool is what fails, not the relay.
Open page