Fix Nginx 'upstream sent invalid header while reading response header from upstream' with PHP-FPM

intermediateโšก Nginx2026-03-18| Nginx 1.18+, PHP-FPM 7.4/8.x, Ubuntu 20.04/22.04, Debian 11/12

Error Message

upstream sent invalid header while reading response header from upstream
#nginx#php-fpm#upstream#header#fastcgi

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_pass target โ€” 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_index directives

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 = On in php.ini โ€” it absorbs accidental early output before PHP touches headers, preventing this whole class of error
  • Keep display_errors = Off in 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 -t in your CI/CD pipeline before pushing any config changes
  • Set pm.max_requests = 500 in pool config โ€” workers get recycled after 500 requests, which stops memory-leaked workers from sending garbage output

Related Error Notes