# How to Fix 502 Bad Gateway in nginx, Step by Step

A [502 Bad Gateway](/http/502) from nginx always means the same thing: nginx is fine, and whatever it forwards requests to — Node, PHP-FPM, Gunicorn, a container — didn't give it a usable answer. The good news is that nginx writes down exactly what went wrong. The fix starts in its error log.

## Step 1: read the error log

```bash
sudo tail -n 50 /var/log/nginx/error.log
```

Reload the failing page while you watch, and match the message to its cause:

| Message in error.log | Usual cause |
|---|---|
| `connect() failed (111: Connection refused) while connecting to upstream` | The app isn't running, or isn't listening where `proxy_pass` points |
| `connect() to unix:/run/php/php8.2-fpm.sock failed (2: No such file or directory)` | PHP-FPM is stopped, or the socket path or version is wrong |
| `connect() to unix:… failed (13: Permission denied)` | nginx can't open the socket file, or SELinux blocks the connection |
| `upstream prematurely closed connection while reading response header` | The app crashed or was killed mid-request |
| `upstream sent too big header while reading response header` | Response headers (often cookies) exceed nginx's buffers |
| `no live upstreams while connecting to upstream` | Every server in the `upstream` block is marked as failed |

## Step 2: talk to the app directly

From the server itself, skip nginx and call the app on its own port:

```bash
curl -i http://127.0.0.1:3000/
```

If this fails too, nginx was right: fix the app first. If it works, the problem is the connection between nginx and the app.

## Fix: the app isn't running

```bash
sudo systemctl status myapp
sudo journalctl -u myapp -n 100 --no-pager
```

For Docker, `docker ps` and `docker logs <container>`. Look for a startup crash — a missing environment variable or a port already in use are the classics.

## Fix: nginx points at the wrong place

Compare `proxy_pass` with what the app actually listens on:

```bash
sudo ss -ltnp | grep -E ':3000|:8000'
```

One subtle trap: `proxy_pass http://localhost:3000` makes nginx try both IPv6 (`::1`) and IPv4 (`127.0.0.1`). If your app only listens on IPv4, some requests fail with *connection refused* on `[::1]:3000` — intermittent 502s that are maddening to track down. Write the address explicitly:

```nginx
proxy_pass http://127.0.0.1:3000;
```

Inside Docker Compose, `127.0.0.1` is the nginx container itself — use the service name instead (`proxy_pass http://app:3000;`).

## Fix: PHP-FPM socket problems

The socket path includes the PHP version, and upgrades change it. List what exists:

```bash
ls /run/php/
```

Then point nginx at the real file:

```nginx
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
```

For *permission denied*, check the pool config (`/etc/php/8.3/fpm/pool.d/www.conf`): `listen.owner` and `listen.group` should be the user nginx runs as (often `www-data`), with `listen.mode = 0660` — owner and group can read and write, everyone else can't ([what 660 means](/chmod/660)).

## Fix: the app crashes mid-request

*Upstream prematurely closed connection* means the process died or hung up while handling the request. Check the app's logs for the failing request, and check whether the kernel killed it for using too much memory:

```bash
sudo dmesg -T | grep -i -E 'killed process|out of memory'
```

## Fix: headers too big

Large cookies or long auth tokens can overflow nginx's default buffers:

```nginx
proxy_buffer_size 16k;
proxy_buffers 8 16k;
proxy_busy_buffers_size 32k;
```

For PHP, the equivalents are `fastcgi_buffer_size` and `fastcgi_buffers`.

## Fix: SELinux (RHEL, CentOS, Fedora)

If the log says *permission denied* while connecting to a TCP port, SELinux may be stopping nginx from making network connections:

```bash
sudo setsebool -P httpd_can_network_connect 1
```

## Intermittent 502s under load

If 502s come and go, suspect keep-alive. When nginx keeps connections to the app open (an `upstream` block with `keepalive`), it can reuse a connection at the exact moment the app closes it for being idle. Node.js closes idle connections after 5 seconds by default, so make the app's timeout longer than nginx's:

```js
server.keepAliveTimeout = 65_000;   // longer than nginx's upstream keepalive_timeout (60s)
server.headersTimeout = 66_000;
```

## After every change

```bash
sudo nginx -t && sudo systemctl reload nginx
```

`nginx -t` catches syntax errors before they take the site down. If the 502 has become a [504](/http/504) instead, nginx now reaches the app but it's too slow — see [502 vs 503 vs 504](/blog/502-vs-503-vs-504) for that side of the story. And to recheck a batch of URLs once you're done, paste them into the [HTTP Status Checker](/tools/web/http-status-checker).
