
A 502 or 504 usually isn’t “an Nginx problem.” It’s your web tier saying it can’t get a clean, timely response from the upstream.
In this VPS troubleshooting tutorial, you’ll pinpoint the exact failure mode on an Nginx + PHP-FPM stack. You’ll apply the right fix, then verify it with checks you can trust.
This guide assumes Ubuntu 24.04/26.04-class setups or Debian 12/13-class setups. It targets Nginx plus PHP 8.2–8.4 with PHP-FPM.
The workflow fits most VPS and dedicated servers. On AlmaLinux/Rocky, paths and service names may differ.
Before you touch anything: confirm what “502/504” means on your box
First, identify which component is throwing the error. Also note whether it’s constant or intermittent.
Don’t reload services yet. You want the evidence intact.
- 502 Bad Gateway: Nginx didn’t receive a valid response from upstream (often PHP-FPM socket/port issues, crashed workers, or permissions).
- 504 Gateway Timeout: Nginx waited too long for upstream (often slow PHP, tight timeouts, saturated PHP-FPM workers, database latency, or disk I/O stalls).
Quick baseline checks (30 seconds)
Run these on the server while the problem is happening:
date
uptime
free -h
df -h
df -i
ss -lntp | sed -n '1,12p'
If df -h shows your root filesystem or /var near 100%, deal with disk pressure first.
Many 502/504 incidents start with “no space left on device.” They then cascade into socket failures and stuck services.
If that’s what you see, use the checklist from VPS disk space troubleshooting tutorial. Return here after you’ve freed space safely.
VPS troubleshooting tutorial step 1: reproduce the error with a request ID
You need one failing request you can trace across logs. Use curl from the VPS itself.
This avoids CDN behavior and client-side caching.
curl -I -sS http://127.0.0.1/ | cat
curl -I -sS https://yourdomain.tld/ --resolve yourdomain.tld:443:127.0.0.1 | cat
If the error only appears under load, run a small, controlled burst test (be gentle on production):
sudo apt-get update && sudo apt-get install -y apache2-utils
ab -n 200 -c 20 https://yourdomain.tld/
While it fails, pull Nginx error logs for the same time window:
sudo tail -n 200 /var/log/nginx/error.log
sudo tail -n 200 /var/log/nginx/your-site-error.log 2>/dev/null || true
How to interpret common Nginx upstream errors
connect() to unix:/run/php/php8.3-fpm.sock failed (2: No such file or directory)→ socket path mismatch, or PHP-FPM isn’t running.connect() ... failed (13: Permission denied)→ Nginx can’t access the socket due to ownership/mode/SELinux/AppArmor rules.upstream prematurely closed connection while reading response header→ PHP crashed, hit a fatal error, ran out of memory, or got killed by OOM.upstream timed out (110: Connection timed out) while reading response header→ PHP-FPM is saturated or the app is slow; your timeouts are too low for reality.
Step 2: verify Nginx ↔ PHP-FPM wiring (socket vs TCP)
Open your site config and find the PHP handler. On Ubuntu/Debian, it’s typically in /etc/nginx/sites-available/your-site or an included snippet.
sudo nginx -T | sed -n '1,200p' # prints full config; useful for finding the active fastcgi_pass
You’re looking for something like:
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
Confirm the socket exists and who owns it
ls -l /run/php/
stat /run/php/php8.3-fpm.sock
On a healthy setup, the socket is owned by www-data. The mode should allow Nginx’s user (often also www-data) to connect.
Confirm PHP-FPM is running and bound to that socket
systemctl status php8.3-fpm --no-pager
ss -xl | grep php8.3-fpm || true
If PHP-FPM isn’t running, check its logs before you change anything:
sudo journalctl -u php8.3-fpm -n 200 --no-pager
sudo tail -n 200 /var/log/php8.3-fpm.log 2>/dev/null || true
Step 3: fix the “socket not found” and “wrong PHP version” class of failures
This is the most common 502 after upgrades, distro updates, or switching PHP versions.
Scenario A: Nginx points to php8.3, but you run php8.2 (or 8.4)
List installed FPM services:
systemctl list-units --type=service | grep -E 'php.*fpm'
Update the site config to the correct socket. Then test and reload:
sudo nginx -t
sudo systemctl reload nginx
Prefer a reload over a restart unless you have a reason. Reloads keep existing connections steadier.
Scenario B: PHP-FPM is running, but it’s configured for TCP while Nginx expects a socket
Check the pool’s listen setting. The default pool config on Ubuntu is:
sudo grep -R "^listen" -n /etc/php/*/fpm/pool.d/www.conf
If you see listen = 127.0.0.1:9000, adjust Nginx to:
fastcgi_pass 127.0.0.1:9000;
Or switch PHP-FPM back to a socket. On a single-host VPS, sockets are usually simpler and slightly lower overhead.
If you edit www.conf, reload PHP-FPM:
sudo systemctl reload php8.3-fpm
Step 4: fix permission-denied errors on the PHP-FPM socket
If Nginx logs show (13: Permission denied), don’t “chmod 777” the socket.
Fix the pool configuration so it survives restarts and package updates.
On Ubuntu/Debian: set listen owner/group/mode
Edit the active pool file (often /etc/php/8.3/fpm/pool.d/www.conf):
sudo nano /etc/php/8.3/fpm/pool.d/www.conf
Set:
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
Then reload PHP-FPM and Nginx:
sudo systemctl reload php8.3-fpm
sudo nginx -t && sudo systemctl reload nginx
AppArmor edge case (rare, but real)
If filesystem permissions look correct and you still see denies, check AppArmor:
sudo aa-status
sudo journalctl -k | grep -i apparmor | tail -n 40
Most VPS setups don’t need custom profiles. If you’re unsure, keep the socket under /run/php/ using distro defaults.
Avoid moving it into a custom location.
Step 5: diagnose timeouts (504) and saturated PHP-FPM pools
A 504 means PHP didn’t answer quickly enough. Common causes are slow code, too few workers, or a stalled dependency (often the database).
Check PHP-FPM pool status and backlog indicators
Start with PHP-FPM’s journal and logs. Look for lines like server reached pm.max_children or pool seems busy.
sudo journalctl -u php8.3-fpm -n 300 --no-pager | tail -n 120
sudo grep -R "max_children" -n /var/log/php8.3-fpm.log 2>/dev/null | tail -n 20 || true
Then check how many PHP-FPM processes exist during load:
ps -o pid,ppid,cmd,%mem,%cpu -C php-fpm8.3 --sort=-%cpu | head -n 20
Right-size pm.max_children (simple method that works)
On most VPS plans, RAM sets the limit long before CPU does. Use a rough sizing pass that keeps you out of swap:
- Measure typical PHP worker RSS for your app (for example, 80–140 MB per worker for WordPress with common plugins).
- Reserve memory for the OS, Nginx, your database (if local), and filesystem cache.
- Set
pm.max_childrenso worst-case worker memory doesn’t force swapping.
Example: on a 4 GB VPS running Nginx + PHP-FPM with a remote database, reserve ~1.2 GB for OS/cache. That leaves ~2.8 GB.
If workers average ~110 MB RSS, max children is about 25. If your traffic spikes sharply, stay a little under that.
Apply a practical pool configuration
Edit /etc/php/8.3/fpm/pool.d/www.conf. Use dynamic unless you have a specific reason to pin workers:
pm = dynamic
pm.max_children = 25
pm.start_servers = 6
pm.min_spare_servers = 4
pm.max_spare_servers = 10
pm.max_requests = 500
pm.max_requests helps long-running sites by recycling workers. It also limits memory bloat over time.
Reload PHP-FPM:
sudo systemctl reload php8.3-fpm
Align timeouts across Nginx and PHP
Misaligned timeouts create confusing failures. If Nginx waits 60 seconds but PHP stops scripts at 30, you’ll see intermittent 504s and half-complete responses.
- Nginx:
fastcgi_read_timeoutin your site config or fastcgi params. - PHP:
max_execution_time(php.ini) andrequest_terminate_timeout(pool).
For WordPress admin tasks (updates, backups, imports), 120 seconds is a reasonable starting point on admin routes only. Don’t loosen the entire public site unless you’ve measured the impact.
Example Nginx snippet for wp-admin (adapt as needed):
location ~* ^/wp-admin/.*\.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_read_timeout 120s;
}
Keep the public site tighter (30–60 seconds). That prevents slow pages from stacking up and exhausting workers.
Step 6: catch OOM kills and “upstream prematurely closed connection”
If the kernel is killing PHP workers, Nginx will report a 502. If you skip system logs, you’ll treat symptoms instead of the cause.
Look for OOM killer messages
sudo journalctl -k --no-pager | grep -i -E 'oom|killed process' | tail -n 50
If you find OOM events, you have a few realistic choices:
- Lower concurrency: reduce
pm.max_childrento prevent memory spikes. - Add RAM: for production WordPress/WooCommerce, moving from 2 GB to 4 GB often stops peak-time crashes.
- Use full-page caching: reduce PHP requests per second so workers aren’t perpetually busy.
If caching is on your roadmap, pair this tutorial with Nginx FastCGI cache setup guide. For many WordPress VPS setups, it’s the highest-ROI stability fix.
Step 7: verify Nginx buffers and client body limits (the “it fails only on uploads” trap)
Some “504” reports are really upload limits or buffering issues. When buffering breaks, the upstream can stall and look “timed out.”
Check these in your site/server block:
client_max_body_size(uploads)fastcgi_buffers/fastcgi_buffer_size(large headers from apps/plugins)
Example baseline for WordPress sites that produce larger headers (often plugins, auth layers, or complex cookies):
client_max_body_size 128m;
fastcgi_buffer_size 32k;
fastcgi_buffers 16 32k;
Apply, test, reload:
sudo nginx -t && sudo systemctl reload nginx
Step 8: check upstream health from Nginx’s point of view
If you can’t reproduce the failure on demand, add a little observability now.
It makes the next incident obvious instead of mysterious.
Add upstream timing to the access log
In /etc/nginx/nginx.conf (or a logging include), define a log format that includes upstream timing:
log_format upstream_timing '$remote_addr - $host "$request" $status '
'rt=$request_time urt=$upstream_response_time uct=$upstream_connect_time '
'uht=$upstream_header_time ua="$http_user_agent"';
Then in your site config:
access_log /var/log/nginx/your-site-access.log upstream_timing;
Reload Nginx and watch slow requests live:
sudo systemctl reload nginx
sudo tail -f /var/log/nginx/your-site-access.log
If rt is high and urt is also high, PHP is slow. If uct is high, PHP-FPM is struggling to accept connections (pool saturation or socket contention).
Step 9: common fixes that look helpful but usually waste time
- Restarting everything as the first move. You clear the symptom and erase the trail you need.
- Setting huge timeouts globally. You turn a performance problem into a resource-exhaustion problem.
- Blindly increasing pm.max_children on small VPS plans. If RAM is tight, you’ll trade 504s for OOM kills.
- Ignoring disk I/O. A saturated or unhealthy disk can produce 504s even when CPU looks fine.
Step 10: stabilization checklist you can keep in your runbook
Use this after you apply a change. It’s meant to prove the incident is actually resolved.
- Run:
sudo nginx -tand confirm no warnings about duplicate directives or bad includes. - Confirm PHP-FPM is healthy:
systemctl status php8.3-fpm. - Watch logs for 10 minutes:
tail -f /var/log/nginx/error.logandjournalctl -u php8.3-fpm -f. - Check memory pressure:
free -h(swap usage during peak traffic is a warning sign). - Load-test lightly:
ab -n 200 -c 20(or your preferred tool) and confirm consistent 2xx responses. - Write down what changed (file, line, previous value). That note saves hours later.
Where HostMyCode fits if you want fewer late-night incidents
If you’re dealing with recurring 502/504 errors because the server is undersized, move to a plan that matches your traffic.
If you don’t want to keep tuning pools, limits, and logs, do the same.
A HostMyCode VPS gives you predictable resources for Nginx + PHP-FPM. If you’d rather hand off incident hardening and performance tuning, managed VPS hosting is the practical option.
Running WordPress or PHP apps on a VPS is simple until a traffic spike hits the default limits. If you want a stable baseline for Nginx + PHP-FPM (plus help sizing RAM/CPU for your workload), start with a HostMyCode VPS. If you prefer hands-off operations, managed VPS hosting covers the tuning, monitoring, and incident response that keeps 502/504 pages from coming back.
FAQ: fixing 502/504 errors on Nginx + PHP-FPM
Should I use a UNIX socket or TCP for PHP-FPM?
On a single-server VPS, a UNIX socket (/run/php/php8.x-fpm.sock) is usually the simplest option and has slightly lower overhead.
TCP makes sense when the upstream runs on another host or across a container boundary.
What’s a safe fastcgi_read_timeout for WordPress?
Keep the public site at 30–60 seconds. Raise it to 120 seconds only on admin routes that legitimately run longer (updates/imports).
Avoid setting 300+ seconds globally.
How do I know if pm.max_children is too low?
PHP-FPM logs will mention hitting pm.max_children. You’ll also see long upstream connect times in Nginx timing logs.
If you increase it, watch RAM closely. That’s how you avoid OOM kills.
Why did this start right after a PHP upgrade?
Nginx often keeps pointing at the old socket path (for example, php8.2-fpm.sock) while PHP-FPM is now running php8.3/8.4.
Confirm the active fastcgi_pass and the pool listen value match.
What if I also see random SSL/TLS errors during the incident?
Stabilize the upstream first. Then troubleshoot TLS errors separately.
If renewals or certificate chains are involved, follow TLS certificate renewal troubleshooting so you’re not compounding failures.
Summary: a repeatable workflow beats guesswork
Most 502/504 incidents land in a small set of categories: broken socket wiring, permission problems, PHP-FPM saturation, mismatched timeouts, or memory pressure.
Work the steps in order. Reproduce the issue, read Nginx’s upstream error, confirm PHP-FPM health, then tune pools and timeouts.
You’ll fix the cause instead of hiding it.
If you want this stack running on predictable resources, consider a HostMyCode VPS. If you’d rather have an expert handle tuning, monitoring, and recurring incident cleanup, managed VPS hosting is built for that.