A 502 page tells you nothing. It means Nginx asked PHP-FPM for a response and either couldn't reach it or got something it couldn't use. The browser can't tell you which, but the Nginx error log can, in one line. Most 502 problems are solved by reading that line and matching it to a cause, and this article is organized that way.
Every error message below is real output. I built an Nginx 1.24.0 and PHP-FPM 8.3.6 setup on Ubuntu 24.04, caused each failure on purpose, and copied the log lines. Paths use the Ubuntu defaults (/var/log/nginx/error.log, /var/log/php8.3-fpm.log, /run/php/php8.3-fpm.sock); adjust for your distribution.
Step one: read the error log
sudo tail -n 20 /var/log/nginx/error.log sudo tail -n 20 /var/log/php8.3-fpm.log Find the 502 request in the first file. The message after it is your cause:
| Nginx error log says | Likely cause | Go to |
|---|---|---|
connect() to unix:… failed (2: No such file or directory) | PHP-FPM isn't running, or the socket path is wrong | 1 |
connect() … failed (111: Connection refused) | Nothing is listening there (stopped, crashed, wrong port) | 1, 3 |
connect() … failed (13: Permission denied) | Nginx can't open the socket | 2 |
recv() failed (104: Connection reset by peer) while reading response header | The PHP worker died or was killed mid-request | 4 |
connect() … failed (11: Resource temporarily unavailable) | All workers busy and the queue is full | 5 |
upstream sent too big header while reading response header | Response headers exceed Nginx's buffer | 6 |
1. PHP-FPM is down, or the socket path is wrong
The most common one. With PHP-FPM stopped cleanly, the socket file is removed and Nginx logs:
connect() to unix:/var/tmp/fpmtest/php.sock failed (2: No such file or directory) while connecting to upstream (My test socket lived in /var/tmp/fpmtest; yours will be under /run/php/.) The same message appears when PHP-FPM is running but fastcgi_pass points at a different file. Pointing at a socket that doesn't exist gives exactly the same error:
connect() to unix:/var/tmp/fpmtest/php8.2-fpm.sock failed (2: No such file or directory) while connecting to upstream This often follows a PHP upgrade: the package installs php8.3-fpm with a new socket name while the Nginx site still references the old one. The stock Ubuntu 24.04 site template even ships a commented example that still says php7.4-fpm.sock, which is easy to copy by mistake. Check what is actually listening and compare it with your config:
ls /run/php/ ss -xl | grep php grep -R "fastcgi_pass" /etc/nginx/sites-enabled/ grep -n "^listen" /etc/php/8.3/fpm/pool.d/www.conf $ ss -xl | grep php u_str LISTEN php.sock (Trimmed to the useful columns.) The correct Ubuntu pairing is listen = /run/php/php8.3-fpm.sock in the pool and the same path in the site:
location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/php8.3-fpm.sock; } A harder-killed PHP-FPM behaves differently. I used kill -9 on the master process, which doesn't clean up. The socket file stayed on disk, so the message changed:
connect() to unix:/var/tmp/fpmtest/php.sock failed (111: Connection refused) while connecting to upstream A socket file that exists but refuses connections means nobody is behind it. Start the service (sudo systemctl restart php8.3-fpm), then look at /var/log/php8.3-fpm.log to see why it stopped.
2. Permission denied on the socket
PHP-FPM creates the socket with the owner, group and mode from the pool. The PHP manual notes that on Linux, read and write permission on the socket is required for the web server to connect, and that the mode defaults to 0660 with the owner and group set to the user running the pool. If the Nginx worker user isn't that owner or in that group, it can't connect. I broke it deliberately with listen.owner = root, listen.group = root and listen.mode = 0600:
connect() to unix:/var/tmp/fpmtest/php.sock failed (13: Permission denied) while connecting to upstream $ ls -l /var/tmp/fpmtest/php.sock srw------- root root The fix is to make the pool's socket settings match the user from the user directive in nginx.conf (www-data on Ubuntu):
; /etc/php/8.3/fpm/pool.d/www.conf listen.owner = www-data listen.group = www-data listen.mode = 0660 $ ls -l /var/tmp/fpmtest/php.sock srw-rw---- www-data www-data Restart PHP-FPM afterward. The socket is created at startup, so a reload of Nginx alone changes nothing. Also check the directory: Nginx must be able to traverse every directory in the socket's path.
3. A TCP port mismatch
If you use TCP instead of a socket, both sides must agree on the address. With Nginx pointing at 127.0.0.1:9001 and nothing listening there:
connect() failed (111: Connection refused) while connecting to upstream Compare fastcgi_pass 127.0.0.1:9000; with the pool's listen = line. The PHP manual warns that an exposed FastCGI port allows arbitrary code execution, so keep TCP bound to 127.0.0.1 and list allowed clients if you must listen on a network address. On a single host, a Unix socket is the safer choice.
4. The PHP worker dies mid-request
This one is different: the connection succeeds, then the worker disappears before it replies. To reproduce it I had a script kill its own process with signal 11 (segmentation fault):
Nginx: recv() failed (104: Connection reset by peer) while reading response header from upstream PHP-FPM: WARNING: [pool www] child 2585 exited on signal 11 (SIGSEGV) after 1.222179 seconds from start The two logs together are the diagnosis: Nginx says the connection was reset, PHP-FPM says which worker died and from which signal. A real segfault usually comes from a PHP extension or a bug in a PHP build, so look at what changed recently. Signal 9 (SIGKILL) is different. I did not test it, but in practice it often means the kernel's out-of-memory killer, so check dmesg or journalctl -k for "Out of memory" around the same timestamp.
Slow script killed by request_terminate_timeout: a 502, not a 504
request_terminate_timeout (default 0, meaning off) kills a worker that's been serving one request too long. With it set to 2s and a script that sleeps six seconds, the browser sees a 502, not a timeout error:
Nginx: recv() failed (104: Connection reset by peer) while reading response header from upstream PHP-FPM: WARNING: [pool www] child 2611, script '/var/tmp/fpmtest/www/slow.php' (request: "GET /slow.php?s=6") execution timed out (2.114876 sec), terminating PHP-FPM: WARNING: [pool www] child 2611 exited on signal 15 (SIGTERM) after 3.336074 seconds from start The PHP-FPM log names the script and request, so you know exactly what to fix. Compare that with the case where Nginx gives up first, with fastcgi_read_timeout 2s; and no FPM limit:
HTTP 504 upstream timed out (110: Connection timed out) while reading response header from upstream A rule of thumb from these two results: 504 means Nginx stopped waiting, 502 with "Connection reset" means PHP-FPM ended the request or died. Nginx's fastcgi_read_timeout defaults to 60 seconds and applies between two successive reads, not to the whole response. Raising timeouts only hides a slow request; find out why it's slow first. For long exports and reports, queue the work instead.
5. All workers are busy
PHP-FPM can only serve pm.max_children requests at once. Ubuntu's default pool uses pm = dynamic and pm.max_children = 5, which is small for a busy site. I limited the pool to one worker and sent three slow requests at once. By default, extra requests wait in the socket's backlog (the PHP manual gives 511 as the Linux default), so they don't fail at once. They time out:
slow.php?s=5 -> HTTP 504 slow.php?s=5 -> HTTP 504 slow.php?s=5 -> HTTP 504 Nginx: upstream timed out (110: Connection timed out) while reading response header from upstream PHP-FPM: WARNING: [pool www] server reached pm.max_children setting (1), consider raising it The PHP-FPM warning is the confirmation. In my test the symptom was a 504. A 502 appears when the queue itself is full. I set listen.backlog = 1 to make that easy to see, with five simultaneous requests to a one-worker pool:
connect() to unix:/var/tmp/fpmtest/php.sock failed (11: Resource temporarily unavailable) while connecting to upstream In that run two requests got a 502 with that message, two succeeded, and one timed out, so you may see a mix. On a real server the default backlog is large, so you'll normally meet the 504 form first. The fix is the same: serve requests faster, or allow more workers. For memory-based sizing, measure what one worker actually uses under your real application, not an idle one:
ps --no-headers -o rss,cmd -C php-fpm8.3 | grep pool \ | awk '{s+=$1; n++} END {printf "%d workers, average %.0f MB each\n", n, s/n/1024}' 3 workers, average 13 MB each That 13 MB is for a bare test script; a framework application will be much larger, and the number only means something measured on your own app under load. A common rule of thumb is to divide the memory you can spare for PHP by the average worker size and keep a margin, then set pm.max_children below that. Don't raise it past what the database can handle either, since more workers means more connections.
6. The response header is too big
Nginx reads the start of the FastCGI response, the headers, into one buffer (fastcgi_buffer_size, 4k or 8k by default according to the Nginx docs). Large cookies, long redirect URLs, or many Set-Cookie headers can overflow it. A script that sends a 9,000-byte header reproduces it:
upstream sent too big header while reading response header from upstream The obvious fix doesn't work on its own. I set only fastcgi_buffer_size 16k; and Nginx refused to start:
[emerg] "fastcgi_busy_buffers_size" must be less than the size of all "fastcgi_buffers" minus one buffer The Nginx docs say the default fastcgi_busy_buffers_size is the size of two buffers, so it grows with the header buffer. With a 16k header buffer and the default eight 4k response buffers, it no longer fits, so the response buffers have to grow too. This worked:
fastcgi_buffer_size 16k; fastcgi_buffers 16 16k; The request returned 200. This is also why you should always run sudo nginx -t before reloading; the error above would have taken the site down on a restart. If your headers are huge because of cookies, consider whether the application is storing too much in them.
A five-command checklist
sudo tail -n 20 /var/log/nginx/error.log # what Nginx saw sudo tail -n 20 /var/log/php8.3-fpm.log # what PHP-FPM saw ss -xl | grep php # is a socket listening? sudo nginx -t # is the Nginx config valid? sudo php-fpm8.3 -t # is the pool config valid? $ nginx -t -c nginx.conf nginx: the configuration file nginx.conf syntax is ok nginx: configuration file nginx.conf test is successful $ php-fpm8.3 -t -y php-fpm.conf NOTICE: configuration file php-fpm.conf test is successful (My test used custom config paths, hence the -c and -y options; on a normal install just run the commands as in the list above.)
Common mistakes
- Restarting Nginx when PHP-FPM is the problem. Nginx is rarely the broken part; read both logs first.
- Raising
fastcgi_read_timeoutto "fix" a 504. It only hides the slow request. - Changing socket permissions but only reloading Nginx. The socket belongs to PHP-FPM; restart that.
- Forgetting the socket name after a PHP upgrade. It includes the version.
- Setting
pm.max_childrenfrom guesswork. Measure real worker memory and respect the database limits. - Skipping
nginx -t. One bad directive can stop the whole server on restart.
Alternatives
Unix socket versus TCP is a trade-off, not a rule. A socket avoids a network port and is controlled by file permissions, which is why the PHP manual prefers it when the web server is on the same host. TCP is needed when PHP-FPM runs in a separate container or machine. This article's examples use Nginx only; I did not test Apache with PHP-FPM.
Related Dev Talk articles
- Laravel Scheduler Not Running: Cron and schedule:run Fixes
- Laravel .env Configuration: env() vs config() Explained
- How to Detect and Fix Memory Leaks in PHP Applications
Frequently asked questions
What is the difference between 502, 503 and 504?
In this setup, 502 means Nginx couldn't get a valid response from PHP-FPM, 504 means it waited too long (the timeout cases above), and a 500 usually comes from the application itself, such as an uncaught PHP error. I haven't covered 503 here.
Is a 502 always a PHP-FPM problem?
With fastcgi_pass, the upstream is PHP-FPM, so yes in practice, though the cause can be Nginx's own configuration, such as a wrong socket path or buffer size. If you proxy to another app server, the same log-reading approach applies but the messages name that server.
Why does it only happen under load?
Usually worker exhaustion (cause 5): too few workers, or slow requests holding them. Look for the "reached pm.max_children" warning and check what the slow requests are doing.
Should I use a socket or TCP?
A socket on a single host; TCP when PHP-FPM is elsewhere. Make sure Nginx and PHP-FPM agree, and restrict TCP to localhost or trusted addresses.
Sources
- PHP Manual: FastCGI Process Manager (FPM) configuration
- Module ngx_http_fastcgi_module (documentation copy)
Originally published on DEV Talk.



