Skip to content
RU
← All articles

504 Gateway Timeout Fix: nginx, WordPress, AWS, Cloudflare

A server rack with a proxy and backends linked by patch cables

A 504 Gateway Timeout means a proxy, load balancer or CDN waited for the server behind it and gave up before an answer arrived. The request was delivered; the backend was simply too slow, stuck or unreachable within the proxy's timeout. The fix almost always lives on the server side: the slow query, API call or job, not your browser.

Unlike 502, which is a broken or invalid response, 504 is the expiry of a wait — the upstream may still be working on the request after the visitor has already seen the error page. That single fact explains most of the troubleshooting below: find which hop gave up, find what it was waiting for, and make that work faster or move it out of the request.

What does a 504 error mean?

Per RFC 9110 §15.6.5, 504 means the server, acting as a gateway or proxy, did not receive a timely response from an upstream server it needed to access to complete the request. The key word is "timely" — the timeout is configured on the proxy, not on the application. MDN uses the same definition.

A typical modern request crosses several hops, and every one of them has its own clock:

Browser → CDN (Cloudflare / CloudFront) → load balancer (ALB) → nginx → PHP-FPM / Node / Gunicorn → database / external API

The hop whose timeout expires first is the one that writes the error page. The text and styling of that page tell you which hop it was: a bare "504 Gateway Time-out" with "nginx" underneath came from nginx; an AWS-styled page came from CloudFront or the ALB; a branded Cloudflare page usually says 524 instead of 504 (more on that below).

Is a 504 error my fault?

If you are a visitor, almost certainly not. A 504 is generated between servers you do not control. Your computer, browser and cache are not involved in the timeout itself, which is why "clear your cache" advice rarely helps with this code. What a visitor can reasonably do:

  • Wait a minute and reload — many 504s are short spikes of load.
  • Open the page from another network (mobile data instead of Wi-Fi). If it works there, the slow hop may be a corporate or ISP proxy between you and the site.
  • Check whether the site is down for everyone with an external checker (see "How to check" below).
  • If you are on a company network, a 504 page branded with your employer's proxy or security gateway means that proxy timed out — report it to your IT team.

This also answers the common "works on mobile, 504 on desktop" puzzle: the two devices take different network paths (carrier vs home or office network, sometimes IPv6 vs IPv4), and one path goes through a proxy or route that times out.

What does a 504 error mean on a computer?

It means the same thing on Windows, macOS, Linux, Chrome, Edge, Firefox or Safari: the website's own infrastructure timed out. The code is sent by the server; the browser only displays it. Reinstalling the browser or changing computer settings will not make a slow backend faster.

504 vs 502 vs 503 vs 524: which one are you seeing?

CodeWho sends itWhat happenedTypical first check
502 Bad GatewayProxyUpstream answered with garbage, reset the connection or was not listeningIs the app process running? Did a worker crash?
503 Service UnavailableServer or proxyServer is deliberately refusing: overload, maintenance, no healthy targetsMaintenance mode, rate limits, health checks
504 Gateway Time-outProxyUpstream accepted the request but did not answer before the proxy's timeoutSlow query, slow external API, exhausted workers
524 A Timeout OccurredCloudflareCloudflare connected to your origin but got no response within its limitSame as 504, measured at the origin
499 Client Closed Requestnginx (log only)The client (or a proxy in front) hung up before nginx answeredFront proxy timeout shorter than backend time

Detailed guides: fixing 502 Bad Gateway, Cloudflare 524 and nginx 499 — a 499 in nginx logs next to a 504 upstream is a classic sign of mismatched timeouts between two proxies.

6 common causes

  1. Slow SQL query without an index, or waiting on a lock held by another transaction.
  2. Slow external API (payment gateway, search, AI model, geo lookup) called synchronously inside the request.
  3. Suboptimal code — N+1 queries, un-batched operations, loading whole tables into memory.
  4. Heavy work inside the request/response cycle: PDF generation, image processing, sending emails, large exports.
  5. Timeout too low in nginx/CDN/load balancer for an operation that is legitimately long.
  6. Upstream stuck or saturated — database deadlock, all PHP-FPM or Gunicorn workers busy, a hung thread, or a firewall silently dropping packets to the backend.

The last point matters: when every worker is busy, even a fast page waits in the queue until the proxy gives up. A 504 on a trivial URL usually means saturation, not slowness of that URL.

How do I fix 504 Gateway Timeout?

Work from the outside in. Each step narrows down which hop gave up and what it was waiting for.

  1. Reproduce and time it. Measure how long the failing URL takes and at what point it dies:
    curl -o /dev/null -s -w 'code=%{http_code} connect=%{time_connect} ttfb=%{time_starttransfer} total=%{time_total}\n' https://example.com/slow-page
    If total is always close to a round number (30, 60, 100 seconds), you have found the timeout of some hop.
  2. Bypass the front proxy. Request the backend directly from the server to see whether the application itself is slow:
    curl -o /dev/null -s -w '%{http_code} %{time_total}\n' -H 'Host: example.com' http://127.0.0.1:8080/slow-page
  3. Read the proxy error log (next section) to confirm the timeout and see which upstream it was talking to.
  4. Find the slow operation with the application and database slow logs.
  5. Fix the work, then align the timeouts. Raise a timeout only for endpoints that are legitimately long, and move long jobs to a queue.

Fixing a 504 in nginx

nginx logs the exact reason in its error log. The string to search for is:

grep "upstream timed out" /var/log/nginx/error.log | tail -20
# upstream timed out (110: Connection timed out) while reading response header from upstream,
#   client: 203.0.113.7, server: example.com, request: "GET /report HTTP/1.1",
#   upstream: "fastcgi://unix:/run/php/php8.4-fpm.sock"

"While reading response header" means the backend accepted the request and never started answering — a slow application. "While connecting to upstream" means the backend did not even accept the connection in time — it is down, overloaded or firewalled.

To find slow requests in the access log you need timing in the log format; the default combined format has no time field. Add one:

log_format timed '$remote_addr [$time_local] "$request" $status '
                 'rt=$request_time urt=$upstream_response_time';
access_log /var/log/nginx/access.log timed;
# Count 504s by URL
awk '$7 == 504 {print $5}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head
# Requests slower than 5 seconds
grep -E ' rt=([5-9]|[0-9]{2,})\.' /var/log/nginx/access.log | tail -20

More on reading these files in the nginx logs guide.

How to increase the timeout in nginx

The defaults for proxy_read_timeout and fastcgi_read_timeout are 60 seconds (nginx docs). The timeout applies between two successive reads, not to the whole response. Proper configuration:

http {
    # Connection to upstream
    proxy_connect_timeout 10s;
    # Sending request upstream
    proxy_send_timeout 30s;
    # Reading upstream response
    proxy_read_timeout 60s;
    # For FastCGI (PHP-FPM)
    fastcgi_connect_timeout 10s;
    fastcgi_send_timeout 30s;
    fastcgi_read_timeout 60s;
}
# Long-polling endpoint — dedicated location
location /api/long-poll {
    proxy_read_timeout 300s;  # 5 minutes
    proxy_pass http://backend;
}

Raise the value only in the location that needs it, then check and reload: nginx -t && systemctl reload nginx. A global proxy_read_timeout 600s hides problems and lets slow requests pile up and exhaust workers.

Apache as a reverse proxy

With mod_proxy, the wait is governed by ProxyTimeout (it falls back to the global Timeout, 60 seconds by default). The error log shows a line like (70007)The timeout specified has expired: AH01102: error reading status line from remote server. For PHP-FPM behind mod_proxy_fcgi you can set the timeout per backend:

ProxyTimeout 120
<Proxy "unix:/run/php/php8.4-fpm.sock|fcgi://localhost">
    ProxySet timeout=120
</Proxy>

Diagnosing slow requests behind the proxy

# MySQL slow log: top 10 queries
mysqldumpslow -t 10 /var/log/mysql/slow.log
# PHP-FPM slow log with stack traces
# /etc/php/8.4/fpm/pool.d/www.conf:
# slowlog = /var/log/php/www-slow.log
# request_slowlog_timeout = 5s
tail -100 /var/log/php/www-slow.log
# PHP-FPM: are all workers busy? (needs pm.status_path = /status)
# look at "active processes" and "max children reached"
# Node.js: event loop lag
// npm install clinic
// clinic doctor -- node app.js

Check external response time via Enterno.io PageSpeed Check — get TTFB, Web Vitals, and bottlenecks. A consistently high TTFB on normal pages is an early warning of the same slowness that later turns into 504s.

Timeouts must line up: the whole chain

Each layer should wait slightly longer than the layer behind it, so the innermost component fails first with a meaningful error instead of an outer proxy cutting it off blindly. When the inner layer gives up first, nginx usually sees a closed connection and returns 502; when the outer one gives up first, you get 504 while the backend keeps working for nothing.

LayerSettingDefaultWhat you see when it fires
nginxproxy_read_timeout / fastcgi_read_timeout60s504 from nginx
ApacheProxyTimeout (falls back to Timeout)60s504 from Apache
PHPmax_execution_time30sPHP fatal error, often a 500
PHP-FPMrequest_terminate_timeout0 (disabled)Worker killed, nginx returns 502
Gunicorn--timeout30sWorker killed, usually 502
CloudflareProxy read timeout100s524 page from Cloudflare
AWS ALBIdle timeout60s504 from the load balancer
AWS CloudFrontOrigin response timeout30s504 from CloudFront

PHP-FPM: max_execution_time

; php.ini
max_execution_time = 30
max_input_time = 60
; pool config: hard kill for runaway requests
request_terminate_timeout = 120s
// For a large data export, per request:
set_time_limit(300);  // 5 minutes
// But better — move it to a queue!

A trap worth knowing: on Linux and macOS, max_execution_time counts only the script's own execution — time spent in database queries, sleep and stream operations is not included (PHP manual). A script stuck on a slow query can therefore run far past 30 seconds, and nginx answers 504 long before PHP complains. request_terminate_timeout is the wall-clock limit.

Node.js: server timeout

const server = app.listen(3000);
server.setTimeout(60000);  // 60 seconds
server.keepAliveTimeout = 61000;
server.headersTimeout = 65000;  // must be > keepAliveTimeout
// For Fastify
const app = Fastify({
  connectionTimeout: 60000,
  keepAliveTimeout: 61000,
});

Keep keepAliveTimeout above the idle timeout of the load balancer in front; otherwise Node closes idle connections the balancer still considers open, which shows up as sporadic 502s.

504 on AWS: ALB and CloudFront

An Application Load Balancer returns 504 when a target does not respond before the idle timeout (60 seconds by default, adjustable in the load balancer attributes), when the target closes the connection before the timeout, or when it cannot establish a connection to the target at all. Check the ALB access logs: target_processing_time near the idle timeout means a slow app; a value of -1 means the target never answered.

CloudFront waits for the origin according to the origin response timeout in the origin settings (30 seconds by default). AWS documentation also lists a non-obvious cause: CloudFront returns 504 when traffic to the origin is blocked by a firewall or security group, or when the origin is not reachable on the configured port. Test the origin from outside CloudFront before touching timeouts:

curl -sv -o /dev/null https://origin.example.com/ 2>&1 | grep -E 'Connected|HTTP/'

Cloudflare 504 — specifics

Cloudflare waits up to 100 seconds for your origin to respond on Free, Pro and Business plans. When that limit expires, Cloudflare shows its own 524 page, not 504. If you see a plain 504 on a Cloudflare-proxied site, it was almost always produced by your origin's nginx, Apache or load balancer and passed through. Options for legitimately long operations:

  • On the Enterprise plan the proxy read timeout can be raised (up to 6000 seconds).
  • Serve long endpoints from a separate subdomain set to DNS only (not proxied), so Cloudflare is not in the request path.
  • Optimize the code to finish well under 100 seconds, or return 202 and process in the background (the right approach).

504 errors in WordPress

On WordPress the usual suspects are a slow plugin, a heavy admin-ajax.php call, WP-Cron jobs running on page load, big imports or backups, and all PHP workers busy on a small hosting plan. With WP-CLI you can test quickly:

wp plugin list --status=active
wp plugin deactivate --all       # test: does the 504 disappear?
wp plugin activate plugin-name   # re-enable one by one
# wp-config.php: stop WP-Cron from running on visitor requests,
# then call wp-cron.php from a real system cron job
define('DISABLE_WP_CRON', true);

If there is no shell access, rename wp-content/plugins over SFTP to disable all plugins at once. For continuous checks of a WordPress site see monitoring WordPress sites.

Right solution: offload to background

Raising timeouts forever is an anti-pattern. The right solution is offloading long operations to a queue.

// Bad: blocks the request
app.post('/generate-report', async (req, res) => {
  const pdf = await generateBigPDF(req.body);  // 3 minutes
  await sendEmail(pdf);
  res.json({ok: true});
});
// Good: queue + notification
app.post('/generate-report', async (req, res) => {
  const jobId = await queue.add('generate-report', req.body);
  res.status(202).json({jobId, status: 'queued'});
});
// Worker processes in background
queue.process('generate-report', async (job) => {
  const pdf = await generateBigPDF(job.data);
  await sendEmail(pdf);
});

The client receives 202 Accepted + jobId and polls /jobs/:id or gets a WebSocket/webhook notification.

Optimizing slow DB queries

-- Find long queries
SHOW PROCESSLIST;
SHOW FULL PROCESSLIST;
-- EXPLAIN a query plan
EXPLAIN SELECT * FROM orders WHERE user_id = 1 ORDER BY created_at DESC LIMIT 10;
-- Add an index if needed
CREATE INDEX idx_orders_user_created ON orders(user_id, created_at DESC);
-- Check table sizes
SELECT table_name, ROUND(data_length/1024/1024) AS mb
FROM information_schema.tables
WHERE table_schema = 'your_db'
ORDER BY data_length DESC LIMIT 10;

In PostgreSQL the equivalent of the process list is SELECT pid, now() - query_start AS runtime, state, query FROM pg_stat_activity ORDER BY runtime DESC;

How to check

  • HTTP header and availability check — see the exact status code, which server answered (the Server and Via headers reveal nginx, Cloudflare or CloudFront) and the response time from outside your network.
  • Speed check — TTFB and load timing to spot the slowness before it becomes a timeout.
  • Uptime monitoring — periodic checks from several regions with alerts, so an intermittent 504 is caught with a timestamp you can match against your logs.

FAQ

How do I fix a 504 error quickly?

Find the hop that timed out (error page and proxy error log), find the slow work behind it (slow query log, PHP-FPM slow log, APM), fix or queue that work, and only then raise the timeout for the specific endpoint that is legitimately long.

How to remove a 504 error as a visitor?

You cannot fix the server, but you can reload after a minute, try another network, and check whether the site is down for everyone. If it is, only the site owner can resolve it.

How does 504 differ from 502?

504 — upstream did not respond in time. 502 — upstream responded invalidly or closed the connection.

What timeout is adequate?

Web requests: 30-60s. APIs: 10-30s. Long-polling: 300+s. Long-running tasks: move to a queue.

How to detect 504 automatically?

Enterno.io Monitors with short timeouts trigger alerts. Also add APM (Sentry, New Relic).

Does 504 affect SEO?

Yes, if it persists. Googlebot slows crawling when it gets 5xx errors, and URLs that keep returning server errors can drop out of the index. Short, rare incidents are recovered after the next successful crawl.

Conclusion

504 is not a timeout bug — it is a symptom of slow code, a slow external API or saturated workers. Monitor the slow log, optimize queries, offload long work to a queue, and line up timeouts so the innermost layer fails first. Set timeouts generously but not infinitely. Hook up Enterno.io to automatically catch 504s across regions.

Check your website right now

Check your site's HTTP status →
More articles: HTTP
HTTP
404 Not Found: Meaning (RFC 9110) and How to Fix It
15.04.2026 · 2 293 views
HTTP
HTTP Methods Explained: GET, POST, PUT, DELETE and Beyond
16.03.2026 · 1 180 views
HTTP
Server-Sent Events vs WebSockets: Which to Use for Realtime
16.03.2026 · 1 117 views
HTTP
HTTP Request Lifecycle: DNS, TCP, TLS, Request, Response
16.03.2026 · 1 039 views