An HTTP 504 Gateway Timeout in Nginx occurs when Nginx acts as a reverse proxy or FastCGI gateway and does not receive a timely response from the upstream backend (such as PHP-FPM) within the configured timeout window.
In a standard PHP-FPM architecture, Nginx successfully establishes a connection to the FastCGI socket, but PHP-FPM fails to return HTTP response headers before fastcgi_read_timeout (default: 60 seconds) expires.
Quick 30-Second Triage Table
| Symptom / Error Log Signature | Primary Culprit | Configuration Directive / Fix Action |
|---|---|---|
upstream timed out (110: Connection timed out) while reading response header | PHP script ran longer than Nginx read timeout | Increase fastcgi_read_timeout 180s; in Nginx site config |
server reached pm.max_children setting (X), consider raising it | All PHP-FPM worker processes are busy | Increase pm.max_children in /etc/php/8.x/fpm/pool.d/www.conf |
PHP-FPM listen.queue continuously > 0 | Socket backlog full; requests waiting for workers | Increase pm.max_children and align listen.backlog = 2048 |
Worker processes hang in poll() / recvfrom() | Slow database queries or hung external cURL API calls | Enable PHP-FPM slowlog and add explicit cURL timeouts |
| High swap usage / Kernel OOM kills workers | Memory leaks in PHP scripts | Configure pm.max_requests = 500 to recycle workers |
Nginx returns 502 Bad Gateway instead of 504 | PHP-FPM worker terminated or crashed mid-request | Check request_terminate_timeout vs max_execution_time |
1. How the Nginx ↔ PHP-FPM FastCGI Pipeline Works
To diagnose why a gateway timeout occurs, you must understand how Nginx passes requests to PHP-FPM across the Linux socket layer:
Nginx to PHP-FPM Request Flow
┌──────────────────┐
│ HTTP Client │
└────────┬─────────┘
│ (1) HTTP/HTTPS GET /index.php
▼
┌──────────────────┐
│ Nginx Master │ ──( Buffers request headers & body )
└────────┬─────────┘
│ (2) Binary FastCGI Packets over UNIX Socket (/run/php/php-fpm.sock)
▼
┌──────────────────┐
│ PHP-FPM Master │ ──( Dispatches connection to worker queue )
└────────┬─────────┘
│ (3) Assigns to Worker Thread
▼
┌──────────────────┐
│ PHP-FPM Worker │ ──( Executes PHP Bytecode, OPcache )
└────────┬─────────┘
├────► MySQL Database Query (Blocks on I/O)
├────► Redis / Memcached Session Fetch
└────► Outbound External HTTP cURL (Blocks on Network)
- Client to Nginx: The web visitor sends an HTTP request. Nginx terminates SSL/TLS and inspects the routing rules.
- Nginx to Socket: Matching
.phplocations are serialized into binary FastCGI records and transmitted over a local UNIX domain socket (/run/php/php8.3-fpm.sock) or local TCP socket (127.0.0.1:9000). - FPM Master Dispatch: The PHP-FPM master process accepts the FastCGI stream and assigns it to an idle worker child process.
- Execution & I/O: The worker executes PHP bytecode. If the script queries MySQL, writes to disk, or calls an external payment gateway API, the PHP worker thread blocks synchronously until data returns.
- Timeout Trigger: If the worker does not write response bytes back to the socket before Nginx's
fastcgi_read_timeouttimer hits zero, Nginx tears down the connection and sends an HTTP504 Gateway Timeoutto the client.
2. The Upstream Timeout Hierarchy
A common configuration mistake is setting contradictory timeout values across Nginx and PHP.
For stable request lifecycles, timeouts must obey this strict hierarchical inequality:
$$\text{fastcgi_read_timeout} \ge \max(\text{max_execution_time},; \text{request_terminate_timeout}) + \Delta_{\text{network}}$$
Timeout Hierarchy & Execution Budget
0s 120s 150s 180s
├────────────────────────────────────────────────┼───────────────┼───────────────┤
│ PHP Engine: max_execution_time (120s) │ │ │
├────────────────────────────────────────────────┴───────────────┼───────────────┤
│ PHP-FPM Pool: request_terminate_timeout (150s) │ │
├────────────────────────────────────────────────────────────────┴───────────────┤
│ Nginx Gateway: fastcgi_read_timeout (180s) │
▼ ▼
Safe Execution Window 504 Timeout
Why Nginx 504 vs. 502 Occurs:
- When Nginx Times Out First (
fastcgi_read_timeout< PHP limits): Nginx gives up while PHP is still working. Nginx closes the client socket and returns504 Gateway Timeout. - When PHP-FPM Terminates First (
request_terminate_timeout<fastcgi_read_timeout): PHP-FPM forcibly kills the worker withSIGTERM/SIGKILL. Nginx receives a premature socket closure with empty headers and returns502 Bad Gateway.
Note on PHP
max_execution_time: On Linux/Unix platforms,max_execution_timeonly measures CPU execution time inside PHP. Time spent waiting for system calls, disk I/O, database locks, and external cURL network requests does not count againstmax_execution_time. Therefore, a script configured withmax_execution_time = 30can hang for 10 minutes on a slow database query.
3. The 5 Core Root Causes & Step-by-Step Fixes
Cause 1: fastcgi_read_timeout Is Too Low for Heavy Workloads
Long-running operations (e.g. generating bulk CSV exports, rendering complex PDFs, processing image uploads, or running large database migrations) often exceed Nginx’s default 60-second read timeout.
Fix:
Increase fastcgi_read_timeout inside your specific Nginx location ~ \.php$ block:
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
# Extend timeouts for long-running batch endpoints
fastcgi_connect_timeout 10s;
fastcgi_send_timeout 60s;
fastcgi_read_timeout 300s; # 5 minutes
# Enable FastCGI response buffering
fastcgi_buffering on;
fastcgi_buffer_size 32k;
fastcgi_buffers 16 16k;
fastcgi_busy_buffers_size 64k;
}
Reload Nginx:
nginx -t && sudo systemctl reload nginx
Cause 2: PHP-FPM Worker Pool Exhaustion (Worker Starvation)
If your PHP-FPM pool is configured with pm.max_children = 10, it can only process 10 concurrent requests simultaneously. When the 11th request arrives, it sits in the socket queue.
If all 10 workers are busy executing slow requests, new requests wait in the backlog until Nginx's fastcgi_read_timeout expires, causing cascading 504 timeouts across the entire website.
The SRE Pool Sizing Formula:
Calculate your optimal pm.max_children based on available server RAM and average PHP worker memory:
$$\text{pm.max_children} = \frac{\text{Total Available Server RAM} - \text{RAM Reserved for OS & Database}}{\text{Average PHP Worker Memory Footprint (RSS)}}$$
Step 1: Measure Real-World Worker Memory
Run this command on your live server to calculate the average memory (RSS) of running PHP-FPM processes:
ps -ylC php-fpm --sort:rss | awk '{sum+=$8; count++} END {print "Avg Worker RAM: " sum/count/1024 " MB"}'
Step 2: Update Pool Configuration
Edit /etc/php/8.3/fpm/pool.d/www.conf:
[www]
pm = dynamic
; Sized for a 16GB RAM server (8GB dedicated to PHP, ~150MB per worker)
pm.max_children = 50
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 20
; Recycle workers after 500 requests to prevent memory leak growth
pm.max_requests = 500
; Hard execution limit per request
request_terminate_timeout = 150s
Restart PHP-FPM:
sudo systemctl restart php8.3-fpm
Cause 3: Slow MySQL Queries & Blocking External API Calls
PHP workers block synchronously. If a checkout page makes an external cURL call to a third-party payment gateway or webhook with no timeout, the worker stays frozen indefinitely.
100 PHP Workers Saturated by 3rd-Party Dependencies
┌──────────────────────────────────────────────────────────┐
│ [35 Workers] Stuck on Unindexed MySQL Table Scan │
├──────────────────────────────────────────────────────────┤
│ [30 Workers] Frozen on External CRM API (Hung cURL) │
├──────────────────────────────────────────────────────────┤
│ [25 Workers] Waiting on Stripe Webhook TCP Handshake │
├──────────────────────────────────────────────────────────┤
│ [10 Workers] Left for Normal Traffic (Queue Saturated) │
└──────────────────────────────────────────────────────────┘
Fix: Set Strict cURL and Database Timeouts
In your PHP application code (Guzzle, cURL, PDO), enforce strict connection and transfer budgets:
// Guzzle HTTP Client with bounded timeouts
$client = new \GuzzleHttp\Client([
'timeout' => 5.0, // Total request deadline
'connect_timeout' => 2.0, // TCP connect deadline
]);
// Native cURL with bounded timeouts
$ch = curl_init('https://api.example.com/v1/charge');
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3); // 3 seconds max to connect
curl_setopt($ch, CURLOPT_TIMEOUT, 8); // 8 seconds max for full transfer
Cause 4: Linux Kernel Socket Backlog Drop (somaxconn)
When traffic spikes, the Linux kernel socket backlog can drop incoming FastCGI connections before PHP-FPM can accept them.
The effective backlog is capped by the minimum of PHP-FPM’s listen.backlog and the Linux kernel parameter net.core.somaxconn.
Fix:
-
Increase the Linux kernel socket limit:
echo "net.core.somaxconn = 65535" | sudo tee -a /etc/sysctl.d/99-fpm-backlog.conf echo "net.ipv4.tcp_max_syn_backlog = 65535" | sudo tee -a /etc/sysctl.d/99-fpm-backlog.conf sudo sysctl --system -
Align PHP-FPM pool backlog in
/etc/php/8.3/fpm/pool.d/www.conf:listen.backlog = 65535
Cause 5: Script Memory Leaks and OOM Process Evictions
If a PHP script leaks memory (e.g. processing large database result sets into PHP arrays without pagination), the worker's Resident Set Size (RSS) expands until the Linux kernel Out-Of-Memory (OOM Killer) terminates the process.
Diagnostics:
Check if the kernel killed any PHP processes:
sudo dmesg -T | grep -Ei 'oom|out of memory|killed process'
4. Live Diagnostic Protocol & Triage Commands
When 504 errors hit production, follow this 5-step triage sequence:
Step 1: Inspect Nginx Error Logs
Confirm whether Nginx timed out reading headers or connecting to upstream:
sudo tail -f /var/log/nginx/error.log | grep -Ei 'upstream timed out|504'
Step 2: Enable the PHP-FPM Slow Log
The slow log takes stack traces of scripts running longer than a designated threshold (e.g., 5 seconds).
Edit /etc/php/8.3/fpm/pool.d/www.conf:
request_slowlog_timeout = 5s
slowlog = /var/log/php-fpm/slow.log
request_slowlog_trace_depth = 20
Reload PHP-FPM, then tail the slow log to see the exact PHP file, function, and line number causing the delay:
sudo tail -f /var/log/php-fpm/slow.log
Step 3: Monitor Live PHP-FPM Pool Saturation
Enable the PHP-FPM status page in www.conf:
pm.status_path = /fpm-status
Query it from the command line:
SCRIPT_NAME=/fpm-status SCRIPT_FILENAME=/fpm-status QUERY_STRING=full \
cgi-fcgi -bind -connect /run/php/php8.3-fpm.sock
Look for critical saturation metrics:
active processes==max_children: Pool is completely saturated.listen queue> 0: Incoming requests are backing up in the socket.max children reached> 0: Pool hit its upper ceiling.
Step 4: Trace Hung PHP Workers with strace
Find the PID of a worker consuming 100% CPU or hanging:
pgrep -a php-fpm
Trace its system calls in real-time:
sudo strace -p <PID> -s 1024 -f
- If stuck in
recvfrom(3, ...)→ Waiting on MySQL or external TCP socket. - If stuck in
nanosleep(...)→ Script is sleeping or executing a retry loop. - If stuck in
futex(...)→ Thread lock or OPcache semaphore contention.
5. Hardened Production Configuration Templates
Optimized /etc/nginx/sites-available/app.conf
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
root /var/www/app/public;
index index.php index.html;
access_log /var/log/nginx/app.access.log combined buffer=64k flush=5s;
error_log /var/log/nginx/app.error.log warn;
# Static assets bypass PHP entirely
location ~* \.(css|js|jpg|jpeg|png|gif|svg|ico|webp|woff|woff2)$ {
expires 30d;
access_log off;
add_header Cache-Control "public, no-transform";
try_files $uri =404;
}
# Front Controller
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# PHP-FPM FastCGI Handler
location ~ \.php$ {
try_files $uri =404;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $document_root;
fastcgi_param HTTP_PROXY ""; # Mitigate httpoxy vulnerability
# Upstream Socket
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
# Timeouts
fastcgi_connect_timeout 5s;
fastcgi_send_timeout 60s;
fastcgi_read_timeout 180s;
# FastCGI Buffers
fastcgi_buffering on;
fastcgi_buffer_size 32k;
fastcgi_buffers 16 16k;
fastcgi_busy_buffers_size 64k;
fastcgi_temp_file_write_size 64k;
}
}
Optimized /etc/php/8.3/fpm/pool.d/www.conf
[www]
user = www-data
group = www-data
listen = /run/php/php8.3-fpm.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
listen.backlog = 65535
pm = dynamic
pm.max_children = 40
pm.start_servers = 8
pm.min_spare_servers = 4
pm.max_spare_servers = 16
pm.max_requests = 1000
pm.status_path = /fpm-status
ping.path = /fpm-ping
ping.response = pong
request_slowlog_timeout = 5s
request_slowlog_trace_depth = 20
slowlog = /var/log/php-fpm/slow.log
request_terminate_timeout = 150s
catch_workers_output = yes
rlimit_files = 65535
php_admin_value[memory_limit] = 256M
php_admin_value[max_execution_time] = 120
6. How to Monitor and Prevent 504 Timeouts
A robust production system should detect latency degradation and worker saturation long before users see widespread 504 errors.
End-to-End Latency & Outage Telemetry
[ Multi-Region Probe ] ──( Every 30s )──► [ Nginx Edge Proxy ]
│
▼
[ PHP-FPM FastCGI ]
│
▼
[ MySQL / Redis / APIs ]
1. Monitor Time-to-First-Byte (TTFB) and P99 Latency
A gradual increase in TTFB (e.g. from 200ms to 4.5s) is the #1 leading indicator that your PHP-FPM worker pool is running out of capacity.
2. Multi-Region Synthetic Probing
Using an external monitoring suite like Pingzo, configure multi-region synthetic checks with strict response-time assertions:
- HTTP Status Checks: Immediate alerts if the gateway returns
504or502. - Port Probing: Direct TCP health checks against port
9000(FastCGI) or backend database ports (PostgreSQL5432, MySQL3306, Redis6379). - Dependency Outage Correlation: Correlate application timeouts with third-party cloud outages (e.g. Stripe, AWS, OpenAI, Supabase).
7. Frequently Asked Questions (FAQ)
What is the difference between 502 Bad Gateway and 504 Gateway Timeout in Nginx?
A 502 Bad Gateway means Nginx connected to PHP-FPM, but the PHP worker crashed, exited prematurely, or returned invalid/corrupt data headers. A 504 Gateway Timeout means PHP-FPM is still running, but failed to return any response before Nginx's fastcgi_read_timeout timer expired.
What is the default fastcgi_read_timeout in Nginx?
The default value is 60 seconds. It measures the maximum time between two successive read operations from the FastCGI backend, not the total execution time of the entire HTTP request.
How do I increase the PHP execution time limit without causing 504 timeouts?
You must increase both the PHP execution limits and the Nginx FastCGI timeout in tandem:
- In
php.iniorwww.conf: Setmax_execution_time = 180andrequest_terminate_timeout = 200s. - In Nginx site config: Set
fastcgi_read_timeout 240s;so Nginx never terminates before PHP finishes.
How do I know if my PHP-FPM pool is running out of workers?
Inspect your /var/log/php-fpm/www-error.log for the warning: [pool www] server reached pm.max_children setting (50), consider raising it. You can also check the live /fpm-status endpoint; if active processes equals pm.max_children and listen queue is greater than 0, your worker pool is exhausted.
Does restarting PHP-FPM fix a 504 Gateway Timeout?
Restarting PHP-FPM (sudo systemctl restart php-fpm) clears stuck workers and empties the socket queue, temporarily restoring site availability. However, it does not fix root causes such as unindexed slow database queries, undersized worker pools, or hung third-party API calls.
Why does a 504 timeout only happen during high traffic spikes?
During traffic spikes, request volume exceeds the concurrency limit (pm.max_children). Excess requests accumulate in the socket backlog queue. The time spent waiting in line plus the time spent executing exceeds fastcgi_read_timeout, triggering cascading 504 errors across all visitors.
⚡ Eliminate 504 Gateway Timeouts with Pingzo
Never let an exhausted PHP-FPM worker pool or slow database query take your web applications offline. Monitor your web infrastructure with Pingzo to receive real-time latency alerts, TCP port health checks, and instant WhatsApp, Telegram, and Slack notifications.
🚀 Start Monitoring with Pingzo →
🛠️ Test Your HTTP Status Codes with Free Header Checker →
Stop Finding Out About Outages from Angry Users
Get instant WhatsApp & Discord alerts the second your API, website, or server goes down. Setup in 30 seconds with 60-second checks.