MonkeyAI troubleshooting · HTTP 504

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.

Do not translate "504" into "the model provider is down." The delay might sit at the relay, an internal route, a network hop, or the model provider. You need logs or request metadata to tell them apart.
How we verified this guide (August 3, 2026): we checked the live model-list authentication response and the production service logs, then compared the advice with RFC 9110, Nginx, and OpenAI documentation. We did not force a slow production request to manufacture a 504. The steps below diagnose a timeout; they do not claim that MonkeyAI currently has one.
ResponseWhat it establishesWhat to check next
401 JSONThe receiving service rejected authenticationBearer header, key, account or IP restrictions
404The route or requested resource was not foundBase URL, endpoint path and Model ID
429A documented rate or quota limit blocked the requestError code, limit headers and Retry-After when present
504A gateway timed out while waiting upstreamRequest 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

RecordWhy support needs it
UTC time and durationCorrelates the client failure with proxy and upstream logs
Endpoint and Model IDIdentifies the route without exposing the prompt
HTTP status and response bodySeparates a gateway page from a structured API error
Request ID / trace headerProvides the strongest log lookup key when present
stream settingShows whether the timeout happened before the first event or between events
One redacted request shapeShows token/output settings while keeping the API key private
Never paste a full API key into a support ticket or screenshot. Replace it with ***. 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.

For workloads that are expected to run for a long time, use an asynchronous or background API only when the selected endpoint and client explicitly support it. OpenAI documents 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 codeCount in 24hPathLatency signalWhat the log indicates
50247/v1/chat/completions170–570 msUpstream returned 502; gateway surfaced it (account id 16 attached)
503235/v1/chat/completions13–25 msNo account available — fails fast before upstream is even contacted
200(rest of 1457)samevariesHealthy 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
}
Reading the log shape — what to look for:
  • 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 to max_switches account 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's access.log for the matching upstream status and latency; the layer that emits 504 is the one with no further retries to attempt.
What this evidence does not show. The above 24h window had no Nginx 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

  1. If the response includes Retry-After, honor it.
  2. Otherwise wait briefly, then use bounded exponential backoff with jitter.
  3. Retry a failed generation only when duplicate work is acceptable or you have an idempotency strategy.
  4. 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.

Check the client before adding another retry loop. The official OpenAI Python library retries connection errors, 408, 409, 429 and HTTP 5xx responses twice by default. That includes 504. An application-level loop can multiply one visible attempt into several upstream requests. During a controlled diagnostic, set 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 settingDocumented behavior
proxy_connect_timeoutLimits how long Nginx waits to establish the upstream connection; default 60s
proxy_read_timeoutLimits the gap between successive upstream reads; default 60s
proxy_send_timeoutLimits the gap between successive writes to upstream; default 60s
proxy_next_upstreamDoes not include an upstream HTTP 504 response in its default retry conditions
Sub2API operator reference: the upstream project’s current edge-security guide uses 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

API Key troubleshooting

Separate 401, 404 and model errors.

Open page

Base URL /v1

Check missing or duplicated API paths.

Open page

Streaming with SSE

Diagnose buffering, stalled streams and endpoint-specific event formats.

Open page

429 rate limits

Read limit signals and retry without creating a request storm.

Open page

Contact support

Send redacted timing and request evidence.

Open page