The Error
Your Nginx error log will have something like this:
2024/01/15 10:23:41 [error] 12345#12345: *1 upstream sent invalid header while reading response header from upstream, client: 192.168.1.10, server: example.com, request: "GET /index.php HTTP/1.1", upstream: "fastcgi://unix:/run/php/php8.1-fpm.sock", host: "example.com"
The browser gets a 502. This isn't the same as a plain 502 (PHP-FPM isn't running) or a 504 (it timed out). Here, Nginx did connect to PHP-FPM successfully โ but what came back was garbage. PHP-FPM returned something Nginx couldn't parse as a valid FastCGI response header.
Root Causes
Several things can trigger this. The usual suspects:
- PHP script outputs raw text or HTML before headers โ a stray warning, notice, or BOM before any
header()call - Wrong
fastcgi_passtarget โ pointing at an HTTP proxy URL instead of a FastCGI socket or port - Socket path mismatch between Nginx config and what PHP-FPM is actually listening on
- PHP-FPM pool misconfiguration causing workers to crash mid-response
- Missing or incorrect
fastcgi_params/fastcgi_indexdirectives
Step 1: Check PHP-FPM Error Log First
Don't touch Nginx yet. Start with what PHP-FPM is actually doing:
# Ubuntu/Debian
tail -n 50 /var/log/php8.1-fpm.log
# Or by pool (www is the default pool name)
tail -n 50 /var/log/php/8.1/fpm/www-error.log
PHP fatal errors, warnings about output before headers, or segfaults here mean the problem lives in PHP โ not Nginx. Fix that first.
Also check the pool-specific error log. In /etc/php/8.1/fpm/pool.d/www.conf, make sure you have:
catch_workers_output = yes
php_flag[display_errors] = off
php_admin_value[error_log] = /var/log/php8.1-fpm-www.log
Step 2: Verify the fastcgi_pass Target
Nine times out of ten, this is the culprit. Pull up your Nginx server block:
grep -n 'fastcgi_pass' /etc/nginx/sites-enabled/your-site.conf
Common wrong configs:
# WRONG โ pointing at an HTTP proxy instead of FastCGI
fastcgi_pass http://127.0.0.1:9000;
# WRONG โ socket path doesn't exist
fastcgi_pass unix:/var/run/php-fpm.sock;
# CORRECT โ no http://, just host:port
fastcgi_pass 127.0.0.1:9000;
# CORRECT โ socket (verify path exists)
fastcgi_pass unix:/run/php/php8.1-fpm.sock;
Verify the socket exists and PHP-FPM is actually listening on it:
# Check socket file
ls -la /run/php/php8.1-fpm.sock
# Or check TCP port
ss -tlnp | grep php
# Should show: LISTEN ... 127.0.0.1:9000
Then cross-check against what PHP-FPM is configured to use:
grep 'listen =' /etc/php/8.1/fpm/pool.d/www.conf
# listen = /run/php/php8.1-fpm.sock
# or
# listen = 127.0.0.1:9000
The value in fastcgi_pass must match exactly โ character for character.
Step 3: Verify Required FastCGI Directives
A stripped-down but working PHP-FPM location block looks like this:
location ~ \.php$ {
try_files $uri =404;
fastcgi_pass unix:/run/php/php8.1-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
Two directives are easy to miss. Without SCRIPT_FILENAME, PHP-FPM doesn't know which file to execute and returns an empty or malformed response. Without include fastcgi_params, a whole pile of required environment variables never get set.
Make sure that file exists:
ls -la /etc/nginx/fastcgi_params
cat /etc/nginx/fastcgi_params
Step 4: Check for PHP Output Before Headers
Any output before a header() call โ even a single space, a UTF-8 BOM, or a PHP notice โ causes PHP-FPM to send raw content first. Nginx receives that as the response header, fails to parse it, and logs this exact error.
Enable verbose error logging in php.ini to catch these:
# /etc/php/8.1/fpm/php.ini
log_errors = On
error_reporting = E_ALL
display_errors = Off # Never On in production
error_log = /var/log/php8.1-fpm-errors.log
Reload FPM and reproduce the error:
systemctl reload php8.1-fpm
curl -v http://your-domain/problematic-page.php
tail -f /var/log/php8.1-fpm-errors.log
Step 5: Check PHP-FPM Pool Settings
Under heavy load โ say, 50+ concurrent PHP requests on a server with 2GB RAM โ workers can exhaust memory and crash mid-response. That crash produces exactly this error. Check your pool limits:
# /etc/php/8.1/fpm/pool.d/www.conf
pm = dynamic
pm.max_children = 20
pm.start_servers = 5
pm.min_spare_servers = 3
pm.max_spare_servers = 10
pm.max_requests = 500 # Recycle workers after N requests (helps with memory leaks)
Want to watch pool health live? Enable the status page:
# Enable status page in pool config
pm.status_path = /fpm-status
# Then in Nginx:
location /fpm-status {
allow 127.0.0.1;
deny all;
fastcgi_pass unix:/run/php/php8.1-fpm.sock;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
# Query it:
curl http://127.0.0.1/fpm-status
Applying the Fix
Never reload Nginx blind. Test the config first:
nginx -t
# nginx: configuration file /etc/nginx/nginx.conf test is successful
systemctl reload nginx
Same deal for PHP-FPM:
php-fpm8.1 -t
# [15-Jan-2024 10:30:00] NOTICE: configuration file /etc/php/8.1/fpm/php-fpm.conf test is successful
systemctl reload php8.1-fpm
Verification
Watch the Nginx error log live while sending a test request:
tail -f /var/log/nginx/error.log &
curl -I https://your-domain/index.php
A clean response looks like:
HTTP/2 200
content-type: text/html; charset=UTF-8
x-powered-by: PHP/8.1.x
If the upstream sent invalid header line is gone from the error log, you're done.
Prevention
- Set
output_buffering = Onin php.ini โ it absorbs accidental early output before PHP touches headers, preventing this whole class of error - Keep
display_errors = Offin production. PHP warnings written to stdout trigger this exact problem - Pin the socket path in one place and reference it from both PHP-FPM pool config and Nginx config โ add a comment pointing to the other location
- Run
nginx -t && php-fpm8.1 -tin your CI/CD pipeline before pushing any config changes - Set
pm.max_requests = 500in pool config โ workers get recycled after 500 requests, which stops memory-leaked workers from sending garbage output

