VPS Tutorials
How to Fix 502 / 504 Bad Gateway on a VPS (2026): Nginx, PHP-FPM, Gunicorn, and Upstream Timeout Troubleshooting Checklist
When a VPS website suddenly shows 502 Bad Gateway or 504 Gateway Timeout, it is usually not a domain issue. More often, Nginx cannot reach the backend, PHP-FPM/Gunicorn has crashed, Unix socket permissions are wrong, the process pool is exhausted, or the upstream response times out. This article troubleshoots layer by layer through logs, ports, sockets, service status, timeouts, and resource usage.

502 and 504 are the two most common errors in VPS website hosting, and the two most likely to cause panic.
You open the website in a browser and only see:
502 Bad Gateway
504 Gateway Timeout
Many people's first reaction is that the domain is broken, the certificate is broken, or the server has been blocked. In fact, most of the time it is none of these. 502/504 usually means: Nginx is still alive, but the application behind it is not responding properly.
You can think of it as: the doorman is still at the door, but no one in the kitchen is taking orders, or the kitchen is taking orders too slowly.
This article follows a practical order: first confirm the cause from the Nginx logs, then check PHP-FPM, Gunicorn, Node, and Docker upstreams, and finally look at timeouts, process pools, memory, and disk. Do not reinstall the environment right away, and do not just reboot the whole VPS.
Simply put:
- 502 Bad Gateway: Nginx failed to connect to the backend, or the backend returned an invalid response
- 504 Gateway Timeout: Nginx connected, but waited too long for the backend to respond
Common scenarios:
| Error | Common causes |
|---|---|
| 502 | PHP-FPM not started, wrong socket path, port not listening, backend process crashed |
| 502 | Incorrect Unix socket permissions, Nginx cannot access the PHP-FPM socket |
| 502 | Docker container down, wrong reverse proxy port |
| 504 | PHP/application executes too slowly, database stuck, API blocked |
| 504 | Nginx proxy_read_timeout / fastcgi_read_timeout too short |
| 504 | Backend process pool full, requests queued too long |
So the troubleshooting direction is not "the website won't open" but "where in the chain from the front-end proxy to the backend is broken."
Don't guess. Look at the logs first:
sudo tail -n 100 /var/log/nginx/error.log
Watch in real time:
sudo tail -f /var/log/nginx/error.log
Then refresh the web page and see what the new error is.
Common types:
connect() failed (111: Connection refused) while connecting to upstream
This means Nginx tried to connect to the backend port or socket and was refused. The backend may not be started, or the port is wrong.
connect() to unix:/run/php/php8.2-fpm.sock failed (13: Permission denied)
This indicates a socket permission problem.
upstream timed out (110: Connection timed out) while reading response header from upstream
This means the backend responds too slowly, leaning toward 504/timeout.
no live upstreams while connecting to upstream
This means there is no available backend in the upstream group.
Once you understand this line, you basically know where to check next.
If you recently changed the site configuration, check Nginx first:
sudo nginx -t
If the configuration is fine, then reload:
sudo systemctl reload nginx
Do not run restart directly. nginx -t can catch configuration syntax errors in advance, and reload is smoother than restart.
Then check the status:
systemctl status nginx --no-pager
journalctl -u nginx --since "30 minutes ago" --no-pager
The official Nginx documentation also clearly distinguishes the levels of error_log and debug logging. For ordinary troubleshooting, the error log is enough; debug logs suit complex problems, so do not turn them on at the start, otherwise the logs will fill up very quickly.
For PHP websites such as WordPress, Typecho, Laravel, and ThinkPHP, a 502 is most likely related to PHP-FPM.
First check the service name. Different systems may call it:
systemctl list-units | grep fpm
Commonly it is:
php8.1-fpm
php8.2-fpm
php8.3-fpm
Check the status:
systemctl status php8.2-fpm --no-pager
journalctl -u php8.2-fpm --since "30 minutes ago" --no-pager
If it is not started:
sudo systemctl restart php8.2-fpm
But do not stop after restarting. Keep looking at the logs to confirm whether it is a configuration error, insufficient memory, or a failed extension load.
Nginx and PHP-FPM usually communicate through a Unix socket or a TCP port.
First find what is written in Nginx:
sudo grep -R "fastcgi_pass" /etc/nginx/sites-enabled /etc/nginx/conf.d
You may see:
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
Then confirm whether the socket exists:
ls -l /run/php/
If Nginx is configured with php8.1-fpm.sock but the system actually only has php8.2-fpm.sock, that is a typical 502.
If Nginx is configured with TCP:
fastcgi_pass 127.0.0.1:9000;
Then check the port:
ss -lntp | grep 9000
In the official Nginx FastCGI documentation, fastcgi_pass is the key directive that determines which FastCGI backend the request is handed to. If the path or port is wrong, it does not matter how healthy Nginx is.
If the log contains:
Permission denied while connecting to upstream
First check the socket permissions:
ls -l /run/php/php8.2-fpm.sock
Then check the user Nginx runs as:
ps -o user,group,cmd -C nginx
The PHP-FPM pool configuration usually contains these items:
listen = /run/php/php8.2-fpm.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
Common configuration file locations:
/etc/php/8.2/fpm/pool.d/www.conf
After changing, check first and then restart PHP-FPM:
sudo php-fpm8.2 -t
sudo systemctl restart php8.2-fpm
sudo systemctl reload nginx
Commands may differ slightly across distributions. If php-fpm8.2 -t does not exist, you can use:
php-fpm8.2 -tt
Or check the PHP-FPM binary name on the current system.
If website traffic rises, or a PHP request gets stuck, the PHP-FPM process pool may be exhausted.
You may see in the logs:
server reached pm.max_children setting
First check the PHP-FPM logs:
journalctl -u php8.2-fpm --since "1 hour ago" --no-pager
Then look at the pool configuration:
sudo grep -E "^pm\.|^request_" /etc/php/8.2/fpm/pool.d/www.conf
Common items:
pm = dynamic
pm.max_children = 10
pm.start_servers = 2
pm.min_spare_servers = 2
pm.max_spare_servers = 5
request_terminate_timeout = 60s
The official PHP PHP-FPM configuration documentation covers parameters such as pm.max_children, pm.max_requests, request_terminate_timeout, slowlog, and request_slowlog_timeout. Do not just keep raising pm.max_children without limit; it consumes memory.
A rough way to estimate PHP process memory is:
ps -ylC php-fpm8.2 --sort:rss
If each PHP child process uses 80MB and the machine only has 1GB of memory, setting pm.max_children to 50 is very dangerous. It may not fix the 502 and instead push the VPS into OOM.
PHP-FPM officially supports a status page, which shows process pool status such as active processes, listen queue, max children reached, and slow requests.
Add to the pool configuration:
pm.status_path = /fpm-status
ping.path = /fpm-ping
In Nginx, allow only local access:
location ~ ^/(fpm-status|fpm-ping)$ {
access_log off;
allow 127.0.0.1;
deny all;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
After reload, test:
curl http://127.0.0.1/fpm-status
curl http://127.0.0.1/fpm-status?json
curl http://127.0.0.1/fpm-ping
If listen queue is often not 0, or max children reached grows, PHP-FPM cannot keep up.
If you use Django, Flask, or FastAPI, the backend may be Gunicorn or Uvicorn.
First look at the Nginx upstream configuration:
sudo grep -R "proxy_pass" /etc/nginx/sites-enabled /etc/nginx/conf.d
If it is:
proxy_pass http://127.0.0.1:8000;
Then check the port:
ss -lntp | grep 8000
Check the service:
systemctl status gunicorn --no-pager
journalctl -u gunicorn --since "30 minutes ago" --no-pager
If you are not sure of the service name:
systemctl list-units | grep -Ei "gunicorn|uvicorn|app"
ps -ef | grep -Ei "gunicorn|uvicorn|python"
If Nginx can connect to the port but returns 504, you usually need to look at application logs, database connections, slow queries, and third-party API timeouts.
Node applications commonly use Nginx reverse proxy to a local port.
Check Nginx:
sudo grep -R "proxy_pass" /etc/nginx/sites-enabled /etc/nginx/conf.d
Check the port:
ss -lntp | grep -E ':3000|:8080|:8000'
If using PM2:
pm2 status
pm2 logs --lines 100
Common 502 causes:
- Node process crashed
- Port changed but Nginx was not updated
- Environment variables not loaded
- Application starts slowly or hangs
- Memory blew up and the system killed it
Check OOM:
journalctl -k --since "2 hours ago" | grep -Ei "out of memory|killed process"
When running a website in Docker, Nginx may be on the host or in a container.
First check container status:
docker ps
docker compose ps
Check logs:
docker logs 容器名 --tail 100
docker compose logs --tail 100 服务名
Check port mapping:
docker ps --format 'table {{.Names}}\t{{.Ports}}'
If Nginx is configured with:
proxy_pass http://127.0.0.1:3000;
but Docker never mapped 3000 to the host, or the service inside the container listens on 127.0.0.1 instead of 0.0.0.0, you get a 502.
Test inside the container:
docker exec -it 容器名 sh
curl -I http://127.0.0.1:3000
Test on the host:
curl -I http://127.0.0.1:3000
These two results help you determine whether the problem is inside the container or in the port mapping from the container to the host.
If the error is 504, the backend is usually not completely down but too slow.
Common Nginx parameters:
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
fastcgi_connect_timeout 60s;
fastcgi_send_timeout 60s;
fastcgi_read_timeout 60s;
The official Nginx documentation covers parameters such as fastcgi_read_timeout and fastcgi_connect_timeout. But do not change the timeout to 600 seconds the moment you see a 504.
If a page takes 5 minutes to run, the real problem may be:
- Slow database queries
- External API stuck
- File upload too large
- PHP tasks that should not run inside a web request
- Queue not split out
- CPU / IO maxed out
Check resources first:
top
free -m
df -h
df -i
iostat -xz 1
If iostat is not available:
sudo apt install sysstat
Then check application logs and database slow queries. Adjusting the timeout only relieves pain; it does not cure the disease.
When a website reports 502/504, I generally follow this order:
curl -I https://example.com
sudo tail -n 100 /var/log/nginx/error.log
sudo nginx -t
systemctl status nginx --no-pager
systemctl list-units | grep -Ei "php|fpm|gunicorn|uvicorn|docker"
ss -lntp
If it is PHP:
systemctl status php8.2-fpm --no-pager
journalctl -u php8.2-fpm --since "30 minutes ago" --no-pager
sudo grep -R "fastcgi_pass" /etc/nginx/sites-enabled /etc/nginx/conf.d
ls -l /run/php/
If it is a reverse-proxied application:
sudo grep -R "proxy_pass" /etc/nginx/sites-enabled /etc/nginx/conf.d
ss -lntp | grep -E ':3000|:8000|:8080'
journalctl -u gunicorn --since "30 minutes ago" --no-pager
pm2 status
If it is Docker:
docker ps
docker compose ps
docker compose logs --tail 100
First locate the break point in the chain, then decide whether to restart the service, change the socket, tune the process pool, or investigate slow requests.
Restarting Nginx, PHP-FPM, or the application service sometimes restores things, but it may just wipe the scene.
A better order is:
- Capture the Nginx error.log
- Record the backend service status
- Check whether the port/socket exists
- Check for OOM, full disk, full inodes
- Then decide whether to reload/restart
If you just blindly run:
sudo reboot
The machine comes back up and the website works, but you still do not know the root cause. It will come back again at the next peak.
- Confirmed whether it is 502 or 504
- Checked
/var/log/nginx/error.log - Ran
nginx -t - Confirmed the Nginx service is healthy
- For PHP sites, checked PHP-FPM status and logs
- Verified that the socket/port in
fastcgi_passexists - Checked socket permissions and the Nginx/PHP-FPM users
- Checked whether
pm.max_childrenis maxed out - For Python/Node applications, confirmed the process and listening port
- For Docker applications, confirmed container status, logs, and port mapping
- For 504, checked slow requests, database, external APIs, CPU/IO
- Did not rely only on a restart to mask the root cause
The most important idea for 502/504 is: Nginx being reachable does not mean the website is reachable. What you need to check is the chain from Nginx to the backend: cannot connect, no permission, backend down, queue full, or response too slow. Break these layers apart and check them, and the problem usually lands on a specific service quickly.