What a 504 is really telling you
A 504 says the proxy waited for WordPress to respond, the response didn't come within the time budget, the proxy gave up. The difference with 502 matters: 502 means broken response, 504 means no response in time.
The interesting part is that 504s usually have a clear, identifiable culprit somewhere — a single slow query, a third-party API stall, a runaway loop. This playbook is how we find that culprit every time.
The timeout chain
Each layer has its own timeout. The 504 originates at the layer with the shortest timeout that the underlying request exceeded:
Cloudflare: 100s (free), 6000s (Enterprise)
Nginx fastcgi_read_timeout: 60s (default)
PHP max_execution_time: 30s (default)
MySQL max_execution_time: 0 (no limit, default)Knowing which layer fired first narrows the diagnosis instantly.
Cause 1 — A specific PHP request takes >30 seconds
Most common 504. A page on your WordPress site genuinely takes longer than PHP's max_execution_time to render.
Identify the slow request
Enable PHP-FPM slow log:
; /etc/php/8.1/fpm/pool.d/www.conf
slowlog = /var/log/php-fpm/www.slow.log
request_slowlog_timeout = 10sReload PHP-FPM. Reproduce the 504. Examine the slow log — it will name the exact PHP file and line that was running when the timeout hit.
Fix
Once you know the slow code: - If it's a plugin doing heavy work on page load → background it (Action Scheduler, cron) - If it's a slow database query → optimize the query (add index, EXPLAIN, refactor) - If it's a third-party API call → add wp_remote_request timeout: 5s and graceful fallback
For unavoidable slow operations (large exports), increase limits surgically rather than globally:
// At the top of the slow script only
set_time_limit(300);
ini_set('memory_limit', '1G');Cause 2 — Slow MySQL query
The query takes >30s. PHP waits, then times out.
Identify
Enable MySQL slow query log:
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 5;
SET GLOBAL slow_query_log_file = '/var/log/mysql/mysql-slow.log';Reproduce the 504. Examine the slow log.
Fix
- Add indexes — most WordPress slow queries are
wp_postmetascans missing an index on(post_id, meta_key) - Refactor query —
meta_queryarrays often produce inefficient SQL; usetax_queryor custom tables for heavy queries - Switch to caching — cache the result with
wp_cache_setor transients
Cause 3 — External HTTP call inside PHP request
A plugin (social share counter, OEMBED, Mailchimp sync) makes an outbound HTTP call. The remote API is slow or unreachable. Your PHP request blocks until the call times out.
Identify
In Query Monitor → HTTP API Calls, look for outbound calls during page load. Times in seconds = your bottleneck.
Fix
Set sane timeouts for all wp_remote_* calls. Most plugins use defaults of 30+ seconds.
$response = wp_remote_get($url, array(
'timeout' => 5,
));For plugins you don't control, you can override via filter:
add_filter('http_request_timeout', function($t) { return 5; });Cause 4 — Cloudflare's 100-second hard timeout
Even with everything else optimized, Cloudflare's free/pro tier has a hard 100-second timeout on origin requests. Long-running admin operations hit it.
Identify
You see the Cloudflare-branded 504 page (their colors and logo). Check Cloudflare → Audit Logs for "origin connection timeout."
Fix
- Move long operations to background jobs (always the right answer)
- For admin-only operations, bypass Cloudflare on
/wp-admin/paths (Page Rules → Cache Level: Bypass, Disable Performance) - For Enterprise customers, Cloudflare allows raising the timeout
You cannot raise the timeout on Free or Pro plans.
Cause 5 — Blocking lock in the database
A long-running write query holds a lock. Subsequent queries wait, exceed timeout. The waiting queries log 504.
Identify
SHOW PROCESSLIST;
SHOW ENGINE INNODB STATUS\GLook for queries in Locked state or rows with WAITING FOR LOCK.
Fix
- Kill the long query:
KILL <connection_id> - Investigate why a query is taking so long (usually an UPDATE on a non-indexed column)
- Consider switching to
SET TRANSACTION ISOLATION LEVEL READ COMMITTEDfor read queries that don't need locks
Cause 6 — DNS resolution timing out
An outbound HTTP call from WordPress depends on DNS. The server's DNS resolver is slow or unreachable. Resolution alone takes 30s.
Identify
dig +short api.someservice.com
time curl -v https://api.someservice.com/ 2>&1 | grep "Connected\|DNS"If dig is slow or hangs, this is the cause.
Fix
Configure a fast DNS resolver on the server:
# /etc/resolv.conf
nameserver 1.1.1.1
nameserver 8.8.8.8
options timeout:1 attempts:2Or install a local caching resolver (dnsmasq, unbound).
The 504 diagnostic flow
When the next 504 happens:
# Step 1 — when did the 504 happen?
date -d "@$(stat -c %Y /var/log/nginx/error.log)"
# Step 2 — what was running at that time?
tail -200 /var/log/php-fpm/www.slow.log
# Step 3 — what was MySQL doing?
grep -A 10 "Query_time" /var/log/mysql/mysql-slow.log | tail -50
# Step 4 — was the call going outbound?
tcpdump -i any port 443 -nn -c 50 2>/dev/null # while reproducingThis sequence finds the slow component in under 10 minutes.
Common mistakes when diagnosing 504
- Raising every timeout to 600s — hides the slow code, doesn't fix it
- Restarting MySQL when a query is stuck — terminates the lock holder but the cause (bad query) returns next time
- Disabling Cloudflare — makes the site unprotected; the 504 was a symptom not the cause
- "Just upgrade hosting" — most slow-WordPress 504s have a plugin or query cause, not a server cause
When to call a specialist
If you've enabled slow logs and the offending code is in a plugin you can't easily replace, we do this rewrite/optimization work routinely. Most 504-prone WordPress sites can be fixed in 2-4 hours of profiling + optimization.
504 emergency support within minutes. For broader performance issues see WordPress speed recovery.

