A 504 error means a gateway or proxy waited for your application to answer, ran out of patience and returned a Gateway Timeout instead; the app itself may still be running, just too slowly for the clock it was given.
Visitors see a blank apology page, you see a healthy process, and both are telling the truth. The fix is finding which timer expired and why the upstream needed longer than it was allowed.
Table of contents
- What a 504 actually says
- Find the timer that fired
- Raising the timeout, and why that is only a patch
- Find the slow thing behind the timeout
- When the load is the problem, not the code
- Where a managed platform helps with 504s
- A diagnosis checklist you can run in ten minutes
- How this fits the rest of the stack
- FAQ
What a 504 actually says
A 504 is a statement about a conversation between two servers, not about the browser. Your visitor talked to a front door: a reverse proxy, a load balancer, a CDN or an API gateway. The front door forwarded the request to your application, started a timer and waited. The timer ran out before a response arrived, so the front door answered on your behalf with status 504.
That makes it different from its neighbours. A 502 means the upstream answered with something the proxy could not use, or refused the connection. A 503 means the service is unavailable or overloaded and said so. A 504 means silence. The upstream accepted the connection, or never got far enough to refuse it, and nothing came back in time.
Silence has a short list of causes. The application is doing slow work in the request path: a heavy query, a call to a third party, a report generated on demand. The application is alive but starved, with every worker busy and new requests queuing behind them. Or the network between proxy and app is dropping packets, so the proxy retries into a void.
The MDN reference for the status is a good one-page definition if you need to explain it to a colleague. The practical point is simpler: a 504 is a timer you can find and a request you can profile.
Find the timer that fired
Timeouts stack. A request from a browser can pass through a CDN, a load balancer, a proxy such as Nginx and an application server, and each hop has its own limit. The shortest one wins, and the error page comes from whichever hop gave up first.
Start by identifying who generated the response. Look at the response headers. A server header naming your CDN means the edge gave up. One naming your proxy means the proxy did. No header at all, or a bare page, often means a load balancer in front.
Then compare the elapsed time on the failing request with the usual suspects. A request that dies at almost exactly 60 seconds is the classic Nginx default. One that dies at 100 seconds points at a CDN-style limit. One at 30 seconds often points at a platform load balancer. The number is the fingerprint.
upstream timed out (110: Connection timed out) while reading response header from upstream
That line in an Nginx error log is the definitive confirmation. It tells you the proxy connected, sent the request and waited for response headers that never arrived. If instead the line says it failed while connecting to upstream, you are looking at a dead or unreachable app, which is a different problem wearing the same status code.
If you run Nginx yourself, the related posts on the 504 gateway time-out in Nginx and the HTTP 504 status cover the config side in more depth.
Raising the timeout, and why that is only a patch
The tempting fix is to increase the limit. Sometimes it is correct, particularly for a genuinely long operation like a file export or an import. In Nginx the directives are proxy_connect_timeout, proxy_send_timeout and proxy_read_timeout, and the read timeout is almost always the relevant one.
location /api/ {
proxy_pass http://app_upstream;
proxy_connect_timeout 5s;
proxy_read_timeout 120s;
}
Notice that the connect timeout stays short. If the app is down you want to find out in seconds, not after two minutes. Only the read timeout needs to be generous, and only for routes that earn it. Raising it globally hides slow endpoints instead of fixing them.
There is also a ceiling you cannot raise. Browsers, CDNs and load balancers have their own maximums, so a 300 second Nginx setting does nothing if an upstream hop cuts the connection at 100. A request that takes minutes should not be a synchronous request at all. Accept the job, return a 202 with an identifier, do the work in a background worker and let the client poll or receive a webhook. Polling a status endpoint is cheap; holding a connection open for four minutes is not.
Treat timeout increases as an apology to your users, not a solution. They buy you time to find the slow code path.
Find the slow thing behind the timeout
Once you know which route is timing out, instrument it. Log the duration of every request and sort by the slowest percentile, not the average. The average hides the one endpoint that hits the limit once an hour.
The usual culprits are worth checking in order:
- A database query without an index. One missing index turns a 5 millisecond lookup into a multi-second scan once the table grows. Run the query with an explain plan.
- An unbounded call to an outside API. If your handler waits on someone else’s service with no timeout of its own, your request inherits their worst day. Set a client timeout shorter than the proxy timeout so you can fail cleanly.
- A connection pool that is too small. Requests wait in line for a database connection and the proxy times out while they queue.
- Cold or starved workers. A single-threaded process doing CPU work blocks every other request behind it.
The ordering matters because the cheap checks come first. A query plan takes a minute. Profiling a worker takes an afternoon. Do the minute first.
One useful habit: make your own timeouts nest. The database call times out in 5 seconds, the outside API call in 10, the request in 25, the proxy in 30. When something fails, your application returns a clear error with a log line instead of the proxy returning a bare 504 with nothing behind it.
When the load is the problem, not the code
Sometimes no single request is slow. The service is simply overloaded. Response times climb with traffic, queues form, and the proxy starts timing out requests that would have been fine at lower load. The signature is that 504s correlate with traffic spikes and clear on their own.
Check the basics: CPU and memory during the incident, the number of busy workers, the length of any request queue. If CPU sits at the ceiling while the errors happen, you need more capacity or less work per request. If memory is climbing until the process restarts, the 504 is a symptom of out-of-memory kills, and the fix is memory, not timeouts.
Two responses are available. Scale vertically by moving to a bigger instance, or scale horizontally by running more copies behind the load balancer. Horizontal scaling only helps if the app is stateless and the database can take the extra connections. Check the database connection limit before adding instances, because ten app copies with fifty connections each is five hundred connections hitting one database.
Caching removes work instead of adding capacity. If the slow endpoint returns data that is identical for most users, cache it for 30 seconds and the thundering herd disappears. That one change has fixed more 504s than any timeout directive.
Where a managed platform helps with 504s
On your own server you assemble the diagnostics yourself: proxy logs, app logs, metrics, and some way to correlate them. A platform that puts runtime logs and metrics next to the deploy is useful here because the 504 investigation is a correlation exercise. Which deploy did the errors start after? Was CPU pinned at the time?
On RunxBuild, web services keep runtime logs and metrics per deploy, and autoscaling can move a service between a floor and a ceiling plan you choose, scaling up at 80 percent CPU and down at 20. That addresses the load-driven class of 504 directly. It does not fix a slow query, which is still your code to fix, but it removes the guesswork about whether the box was simply too small. The details are in the services autoscaling docs.
A diagnosis checklist you can run in ten minutes
Work through this in order and stop when you find the cause.
- Reproduce the request with curl and time it. Note the exact seconds at which it fails.
- Match that number to a timeout in the CDN, load balancer and proxy.
- Read the proxy error log for the upstream timed out line and the route.
- Check whether the app process is running and whether CPU or memory is pinned.
- Time the slow route’s database queries and outside calls individually.
- Decide: fix the query, add a client timeout, move the work to a background job, or add capacity.
- Only then adjust the proxy timeout, scoped to the one route that needs it.
Write down what you found. A 504 that recurs usually recurs for the same reason.
How this fits the rest of the stack
A 504 is cheap to diagnose and expensive to ignore, because capacity problems come back at the next traffic peak. Before you size a bigger plan to absorb it, put the numbers side by side in the RunxBuild hosting calculator and see what headroom actually costs. When you are ready to run the service where the logs and metrics sit beside each deploy, the RunxBuild dashboard is where a web service gets created.
Useful related references:
- The 504 Gateway Timeout Error, in Plain English
- HTTP 504 Gateway Timeout: Reading the Error as a Map of Your Stack
- 504 Gateway Time-out in Nginx: Your Upstream Was Too Slow, Not Nginx
- Services on RunxBuild
FAQ
What is the difference between a 502 and a 504 error?
A 502 means the proxy received an invalid response or could not connect to the upstream. A 504 means the proxy connected but the upstream never answered in time. Read the proxy error log: connection refused points at a down app, upstream timed out points at a slow one.
Is a 504 error my fault as a visitor?
Almost never. It is a server-side timeout between the site’s own servers. Reloading after a few seconds often works, and if it does not, the site owner needs to look at their upstream or capacity.
How long is the default timeout before a 504?
It depends on the hop. Nginx defaults to 60 seconds for reading an upstream response, and CDNs and load balancers commonly cut requests between 30 and 100 seconds. The failing request’s elapsed time tells you which one fired.
Should I just increase proxy_read_timeout?
Only for a specific route that genuinely needs it, such as an export. Raising it globally hides slow endpoints. For work that takes minutes, return a 202 and process it in a background job.
Can high traffic alone cause a 504?
Yes. When workers are all busy, requests queue and eventually exceed the proxy timeout even though no single request is slow. Add capacity, scale out a stateless service or cache the hot endpoint.