Production

502 Bad Gateway: your app never answered

A 502 is not your code throwing an exception. It is nginx or Apache reporting that it asked your application a question and got nothing usable back. That one distinction decides which half of the stack you spend the next hour in.

18:40, you deploy. 18:41, the site returns 502 Bad Gateway. Nothing in the application log. Nothing in the framework log. For a few minutes it genuinely looks like the code vanished.

What the web server is telling you

Your stack has two halves. In front, a web server: nginx, Apache, a load balancer, Cloudflare. Behind it, your application: PHP-FPM, Gunicorn, a Node process, whatever runs your code. The front half forwards each request to the back half and waits.

A 502 means that forward failed. The connection was refused, or dropped, or the reply was malformed. Your code never ran, or ran and died before it could answer. That is why the application log is empty: from its point of view, nothing happened.

Hold that next to its neighbours and the triage gets short. A 500 means your app ran and threw, so look in the application log. A 504 means it accepted the request and never finished, so look for a slow query or an external call with no timeout. A 502 means the conversation never got started, so look at the process and the connection between the two halves.

The five things that cause it

1. The process is dead. Easily the most common. PHP-FPM is not running, the Node process exited, Gunicorn crashed on startup. Often because of something that happened at deploy: a syntax error at import time, a missing environment variable, a failed migration. The process tried to start, threw, and exited, and nothing restarted it.

2. It is listening somewhere else. nginx points at 127.0.0.1:8000, the app bound to 8001, or to a Unix socket that moved, or only to localhost inside a container while nginx is outside it. Nothing is broken; the two halves are just talking past each other.

3. It has not finished starting. A rolling restart where the old process stops before the new one is ready gives you a few seconds of 502 on every deploy. Harmless if it is seconds, a real outage if the app takes thirty seconds to boot and nothing is checking readiness.

4. The worker pool is exhausted. Under load, every PHP-FPM child is busy, the backlog fills, and new connections are refused. The app is alive and healthy and still returning 502s, which is the most confusing version of this. Look for server reached pm.max_children in the FPM log.

5. The reply was too big for the buffer. This one is rarer and it only ever hits a single endpoint. A large response or oversized headers overflow proxy_buffer_size or fastcgi_buffers, and nginx discards the reply. The giveaway is a 502 on one route while everything else works, reproducible every time.

Confirming it in sixty seconds

There are four checks worth making, in this order. Each one rules out a chunk of the list above.

# 1. Is the process alive?
systemctl status php8.3-fpm        # or: pm2 list / docker ps

# 2. Is it listening where the web server expects?
ss -lntp | grep -E '8000|9000'

# 3. Does it answer if you skip the gateway entirely?
curl -sS -o /dev/null -w '%{http_code}
' http://127.0.0.1:8000/

# 4. What did the gateway say about it?
tail -n 50 /var/log/nginx/error.log

That last one is the real answer and the one people skip. nginx writes a specific reason for every 502: connect() failed (111: Connection refused) is a dead process, upstream prematurely closed connection is a crash mid-request, no live upstreams is every backend marked down. The number on the page is vague; the line in the log is not.

The restart trap

Restarting the app fixes a 502 almost every time, which is exactly the problem. The site comes back, the pressure lifts, and the thing that killed the process is still there. A week later it happens again, usually at a worse hour.

A restart is a treatment, not a diagnosis. Take thirty seconds for the log first: once the process is recycled, the evidence is gone.

Before you restart, grab the last fifty lines of the process manager's log. That is where a crash on startup leaves its traceback, and it is the one window you get.

What to do once it is back

Two things are worth doing, and both are unglamorous. First, make sure something restarts the process automatically: Restart=always in systemd, restart: unless-stopped in Docker, or pm2's default. A crash should cost you seconds rather than lasting until someone notices.

Second, make sure the crash is reported. A process that dies and silently respawns is not fixed, it is hidden. If the only way you learn about it is a customer message, you have a monitoring problem on top of the original bug.

That gap between the crash and someone finding out is the part that costs money, and it is the part Patcherly is built to close. The error is caught as it happens, the fix is drafted against your own code, and nothing touches the server until you approve it. For the 502s that turn out to be ordinary application errors in disguise, the log hunt is laid out in getting the real error out of a 500.

Found something wrong in here? Tell me.

Checking API...
Checking AI Gateway...
Check status
⚠️ Setup Required