Skip to main content

HTTP · troubleshooting

504 Gateway Timeout

The upstream was reachable and never finished in time. Unlike a 502, nothing failed — the proxy gave up waiting, which means the answer is either a slow upstream or a timeout set below how long the work honestly takes.

Run this first

step 1 of 3
sudo grep 'upstream timed out' /var/log/nginx/error.log | tail -20

Look for `while reading response header` = the upstream is slow or queued · `while connecting` = the network path is dropping packets.

The message names the stage that timed out, which separates a network path problem from a slow response.

Work out which cause you have

A few questions to narrow the list. Every answer ends in a command that confirms or rules the cause out — this cannot see your cluster, so nothing here is a certainty until you have checked.

Narrow it down

0 answered · nothing is sent anywhere

Which stage does the proxy say timed out?

The nginx message ends with `while <stage>`, and the stage decides the whole investigation.

Cause space

5 of 5 still possible

  • The operation takes longer than the timeout allowsCommon
  • The upstream has become slower than it used to beCommon
  • The upstream has no worker available to accept the requestCommon
  • Timeouts are not ordered across the chainOccasional
  • It is the connect timeout, not the read timeoutRare

Nothing ruled out yet. Answer the question above and the branches your answer eliminates will strike through here.

Check it with a tool

Or diagnose it manually

In this order. The first command usually contains the whole answer.

  1. Step 1

    sudo grep 'upstream timed out' /var/log/nginx/error.log | tail -20

    The message names the stage that timed out, which separates a network path problem from a slow response.

    Look for `while reading response header` = the upstream is slow or queued · `while connecting` = the network path is dropping packets.

  2. Step 2

    time curl -s -o /dev/null -w '%{http_code} %{time_total}\n' http://<upstream>:<port>/<path>

    Times the upstream directly from the proxy's host, which tells you whether the slowness is real or an artefact of the proxy.

    Look for A duration near or above the proxy's timeout confirms genuine slowness. A fast response means the problem is queueing or the path.

  3. Step 3

    sudo nginx -T 2>/dev/null | grep -E 'proxy_(read|connect|send)_timeout' 

    Establishes what the actual limit is before deciding whether it is wrong.

    Look for The effective `proxy_read_timeout`. Unset means the 60-second default.

Every cause, and how to fix it

Ordered by how often each one turns out to be the answer.

The operation takes longer than the timeout allows

Common

A report, an export, a bulk import or a large upload that genuinely needs more than the limit. Nothing is broken — a synchronous HTTP request is the wrong shape for the work.

Confirm

sudo grep 'upstream timed out' /var/log/nginx/error.log | tail -20

Timeouts clustered on one endpoint rather than spread across the application. A single slow route is a strong signal for this cause.

Fix

  • Make it asynchronous. Accept the request, return 202 with a job identifier, and let the client poll or receive a callback. This is the real fix and it removes the class of problem.
  • If it must stay synchronous, raise `proxy_read_timeout` for that route only rather than globally — a global raise removes the protection everywhere else.
location /api/export {
  proxy_pass http://app;
  proxy_read_timeout 300s;   # this route only
}

Scope it to the location. Raising the global timeout hides genuine slowness across the whole application.

The upstream has become slower than it used to be

Common

The timeout has not changed; the latency has. A missing index after data growth, a dependency that got slower, a query whose plan changed, or contention under load. Here the timeout is working correctly and the latency is the bug.

Confirm

sudo awk '{print $NF}' /var/log/nginx/access.log | tail -1000 | sort -n | tail -20

Whether request durations have shifted upward generally, or only for a few endpoints. A broad shift points at a shared dependency.

Fix

  • Do not raise the timeout. It is the only thing currently telling you about the regression, and raising it converts a visible failure into a slow site.
  • Find what changed — a deploy, a data volume threshold crossed, a dependency's own latency. The timing of onset is usually the strongest clue.

The upstream has no worker available to accept the request

Common

The request is queued rather than being worked on. Every worker is busy — often waiting on something else — so the request sits in a backlog and the proxy times out having received nothing. The upstream looks idle by CPU while being completely saturated.

Confirm

ss -tn state established '( dport = :3000 or sport = :3000 )' | wc -l

A connection count at or near the upstream's configured worker or pool limit, with low CPU. Saturation without CPU load is the fingerprint.

Fix

  • Find what the workers are blocked on. Exhaustion is nearly always a downstream wait — a slow database call, an external API, a lock — rather than genuine compute.
  • Raising the worker count without finding the block moves the queue rather than draining it, and can make the dependency worse.

Timeouts are not ordered across the chain

Occasional

Each hop has its own timeout, and if an inner one is longer than an outer one the outer gives up first — so the client sees a 504 while work continues invisibly downstream. This is how a 504 appears with no upstream error at all.

Confirm

sudo nginx -T 2>/dev/null | grep -E 'proxy_(read|connect|send)_timeout'

The proxy's timeouts compared against the application's own client timeouts for its dependencies. The outer should exceed the inner, not the reverse.

Fix

  • Order timeouts deliberately from the outside in, so the innermost gives up first and can return a meaningful error instead of being abandoned.
  • Give the application a client timeout shorter than the proxy's read timeout, so it fails and reports rather than being cut off mid-flight.

It is the connect timeout, not the read timeout

Rare

Rarer and worth distinguishing, because it points somewhere entirely different. A connect timeout means the TCP handshake never completed — usually a firewall dropping packets silently rather than refusing them. A refusal gives a fast 502; a drop gives a slow 504.

Confirm

sudo grep 'upstream timed out' /var/log/nginx/error.log | grep -o 'while [a-z ]*' | sort | uniq -c

`while connecting to upstream` rather than `while reading response header`. The stage named in the message is the diagnosis.

Fix

  • Treat this as a network path problem, not a performance one. Check security groups, firewall rules and routing between the proxy and the upstream.
  • A silent drop rather than a refusal is the signature of a packet filter, which is why the failure is slow rather than immediate.

Understanding it properly

Skip this if you are mid-incident — the working part of the page is above. Worth reading afterwards, because understanding the mechanism is what stops the next one.

What is actually happening

A 504 is a decision rather than a fault. The connection succeeded, the request was delivered, and the proxy stopped waiting. Something is still probably running on the other side.

That has an important consequence people miss: the work may well complete. A 504 on a request that writes data does not mean the write did not happen, which makes blind retries genuinely dangerous for anything non-idempotent.

There are two honest readings and they need opposite fixes. Either the upstream has become slower than it should be — in which case the timeout is doing its job and the latency is the bug — or the operation legitimately takes longer than the configured limit, in which case the limit is wrong or the operation should not be synchronous at all.

nginx has several timeouts and they fail at different stages: `proxy_connect_timeout` for establishing the connection, `proxy_send_timeout` for writing the request, and `proxy_read_timeout` for waiting on the response. The last one is nearly always the one that fires, and its default is 60 seconds.

How to tell this is your problem
WhereWhat you see
Browser / curl`HTTP/1.1 504 Gateway Time-out` arriving after a consistent delay — often almost exactly 60 seconds.
nginx error log`upstream timed out (110: Connection timed out) while reading response header from upstream`.
Upstream logsThe request logged as *started*, and often as completing successfully some time after the client gave up.

How to know it is actually fixed

  • The request completes within the timeout, and the proxy log stops recording timeouts for that route.
  • If the fix was asynchronous, the endpoint returns promptly with a job identifier and the work completes out of band.
  • The endpoint still completes under load, which is where queueing problems reappear.

Stopping it happening again

  • Order timeouts from the outside in, so the innermost hop fails first and can report a real error rather than being abandoned.
  • Alert on latency percentiles rather than only on timeouts. A 504 is the last stage of a degradation that was visible for a while beforehand.
  • Keep long-running work off the synchronous request path. It is the only fix that does not eventually need the timeout raised again.
  • Make retries idempotent, or do not retry. A 504 does not mean the work did not happen.

Did this get you to an answer?

No text box on purpose — please do not paste production logs anywhere

Sources

Behaviour described here is drawn from official documentation. Where a figure could not be confirmed on an official page it is attributed in the text rather than stated as canonical.

Related

504 Gateway Timeout — causes, diagnosis and fix | DevOps Insights