A 502 Bad Gateway error usually means Nginx was able to accept the client request, but it failed when it tried to talk to the upstream application that should have generated the real response. In practice, that upstream might be PHP-FPM, Apache, Gunicorn, uWSGI, Node.js, a local API on another port, or even another reverse proxy layer.
I treat 502 as an architecture problem first and a configuration problem second. The page shown to the user is simple, but the failure path behind it usually is not. Most of the time I find one of a few patterns: the upstream process is down, Nginx is pointing to the wrong socket or port, permissions on a Unix socket are wrong, the upstream sent an invalid response, or the application crashed before it could answer properly.
When people search for “nginx 502 bad gateway fix,” they often go straight to timeout tuning. I understand why. It is one of the more visible knobs. But 502 is not primarily a timeout error. If the upstream is merely slow, that often becomes a 504 instead. A 502 usually tells me Nginx tried to connect or read a valid response and got something broken, refused, missing, or malformed.
If you want to build a cleaner mental model before digging into logs, read troubleshooting common SSL certificate errors for the same kind of layered approach, and Nginx reverse proxy with SSL termination if your upstream path already has more than one proxy involved.
What a 502 means architecturally
Nginx sits between the client and an upstream server. The client connects to Nginx and gets a response from Nginx. But Nginx often acts as a messenger. It forwards the request to something else and waits for an answer.
That answer path matters:
- Client sends request to Nginx.
- Nginx routes request to upstream.
- Upstream accepts the connection.
- Upstream sends a valid HTTP or FastCGI response.
- Nginx relays that response to the client.
A 502 appears when step 3 or 4 fails in a way that is not simply “it took too long.” For example:
- connection refused to
127.0.0.1:3000 - Unix socket file exists but Nginx cannot access it
- upstream process exited during the request
- upstream returned invalid headers
- FastCGI socket path is wrong
- proxy protocol mismatch, such as talking HTTP to a service expecting HTTPS or gRPC
That is why I rarely change random Nginx directives first. I confirm exactly which layer is failing.
502 vs 504: the distinction that saves time
The difference matters because it changes where I look first.
- 502 Bad Gateway: Nginx could not get a valid response from the upstream.
- 504 Gateway Timeout: Nginx waited too long for the upstream.
If the error log says connection refused, no such file, permission denied, or upstream sent invalid header, that is classic 502 territory. If it says upstream timed out while reading response header, I shift toward the slower-request workflow described in HTTP 504 Gateway Timeout: Nginx and Apache debugging.
I also cross-check the access log. A response generated almost instantly often points to a dead upstream or wrong socket. A response after 30 or 60 seconds usually means a timeout chain somewhere.
Start with the Nginx error log, not the browser
The browser error page is nearly useless for diagnosis. Nginx error logs are where the real story starts.
On Debian and Ubuntu systems, I usually begin with:
sudo tail -n 100 /var/log/nginx/error.log
Or, if the issue is happening live:
sudo tail -f /var/log/nginx/error.log
If Nginx runs under systemd and logs are centralized, I also check:
sudo journalctl -u nginx -n 100 --no-pager
Messages I commonly see with 502 problems include:
connect() failed (111: Connection refused) while connecting to upstream
connect() to unix:/run/php/php8.2-fpm.sock failed (13: Permission denied)
connect() to unix:/run/gunicorn.sock failed (2: No such file or directory)
upstream sent invalid header while reading response header from upstream
recv() failed (104: Connection reset by peer) while reading response header from upstream
Each message points in a very different direction. That is why log-first troubleshooting is so much faster than editing configs blindly.
Confirm the upstream is actually running
This sounds obvious, but I still see people spend an hour on Nginx when the backend process is simply down.
Check listening sockets and ports first:
sudo ss -ltnp | grep -E ':80|:443|:3000|:5000|:8000|:9000'
For Unix sockets:
sudo ss -xl | grep -E 'php|gunicorn|uwsgi'
If you use systemd-managed services, inspect them directly:
sudo systemctl status php8.2-fpm --no-pager
sudo systemctl status gunicorn --no-pager
sudo systemctl status myapp.service --no-pager
And then the journal for the specific backend:
sudo journalctl -u myapp.service -n 100 --no-pager
A common pattern is repeated crashes followed by restart attempts. Nginx only shows the symptom. Systemd often shows the cause.
Validate the upstream outside Nginx
I always test the upstream directly so I know whether the application is healthy independent of the proxy.
For a TCP upstream on localhost:
curl -I http://127.0.0.1:3000/
For a JSON API:
curl -sS http://127.0.0.1:5000/health
For a Unix socket speaking HTTP:
curl --unix-socket /run/gunicorn.sock http://localhost/health
If that direct test fails, Nginx is not your main problem. Fix the app or service manager first.
Common proxy_pass mistakes I run into
A surprising number of 502 errors come from small proxy_pass mistakes that look fine at a glance.
Wrong port or protocol
This is the classic one:
location / {
proxy_pass http://127.0.0.1:3000;
}
If the app actually listens on 127.0.0.1:3001, every request fails.
Another variation is proxying plain HTTP to a service expecting HTTPS:
location / {
proxy_pass https://127.0.0.1:8443;
}
If the upstream expects TLS, use https://. If it expects plain HTTP, use http://. Mixing them often produces invalid upstream responses that surface as 502.
Trailing slash behavior
This is not always a 502 cause, but it can send requests to the wrong path and make debugging messy.
location /api/ {
proxy_pass http://127.0.0.1:5000/;
}
That strips the matched prefix differently than:
location /api/ {
proxy_pass http://127.0.0.1:5000;
}
I verify what the app is actually receiving before assuming the backend is broken.
Missing proxy headers
Some applications behave badly when they do not receive Host, X-Forwarded-For, or X-Forwarded-Proto. It may not cause every 502, but I have seen apps redirect incorrectly, reject requests, or generate broken absolute URLs.
A sane baseline looks like this:
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
For a broader reverse proxy setup, Nginx reverse proxy with Node.js configuration covers the baseline headers I usually keep consistent.
Unix socket vs TCP upstreams
I use both. Each has failure modes worth knowing.
Unix socket advantages and problems
Unix sockets are efficient on the same host and avoid exposing a listening TCP port. That is why PHP-FPM commonly uses them.
Example:
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
}
Common 502 causes with sockets:
- socket path is wrong
- socket file disappeared because the service is down
- Nginx user cannot access the socket
- PHP-FPM pool configuration changed and now uses a different socket name
Validate the socket:
ls -l /run/php/
sudo ss -xl | grep php
TCP upstream advantages and problems
TCP is easier to test with curl, ss, and health checks. It also avoids some file permission surprises.
Example:
upstream app_backend {
server 127.0.0.1:5000;
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://app_backend;
}
}
Common TCP 502 causes:
- wrong port
- process bound to
127.0.0.1but config points elsewhere - local firewall rules or container network issues
- process crash causing connection refused or reset
When I am debugging quickly, TCP often gives cleaner visibility. But I do not switch architectures just because of one error. I fix the actual issue.
Buffer and timeout settings that matter
I avoid treating these as magic cures, but they do matter when the upstream response pattern is unusual.
Useful directives include:
proxy_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
send_timeout 60s;
And for response headers or larger upstream output:
proxy_buffer_size 16k;
proxy_buffers 8 16k;
proxy_busy_buffers_size 32k;
If Nginx reports upstream sent too big header, that is not a dead upstream. It is often large cookies or oversized response headers from the application. In that case, tuning proxy buffers may help, but I also inspect why headers got so large in the first place.
A current practice is to tune only what you can justify from logs. A deprecated habit is copying giant timeout and buffer values from random forum posts without understanding them.
PHP-FPM specific 502 failures
PHP-FPM is one of the most common sources of 502 tickets. The pattern is familiar:
- Nginx points to the wrong socket path
- PHP-FPM service is not running
- pool crashed or exhausted resources
- socket ownership and mode do not let Nginx connect
- script execution or process management is broken upstream
A typical working block looks like this:
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.2-fpm.sock;
fastcgi_read_timeout 60s;
}
Check PHP-FPM service state:
sudo systemctl status php8.2-fpm --no-pager
sudo journalctl -u php8.2-fpm -n 100 --no-pager
Inspect the pool config when permissions look suspicious:
grep -E '^(listen|listen.owner|listen.group|listen.mode|user|group)' /etc/php/8.2/fpm/pool.d/www.conf
I expect values compatible with the Nginx worker user. On Debian and Ubuntu, that is often www-data.
A clean example:
listen = /run/php/php8.2-fpm.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660
user = www-data
group = www-data
After changes:
sudo php-fpm8.2 -t
sudo systemctl restart php8.2-fpm
sudo nginx -t
sudo systemctl reload nginx
If you also need broader PHP deployment context, PHP-FPM with Nginx configuration and tuning is worth keeping nearby.
Invalid upstream responses
Some 502 cases happen even though the upstream is reachable. Nginx connects, but the response is malformed or cut off.
Typical reasons:
- application crashed after accepting the connection
- upstream returned non-HTTP data on an HTTP port
- reverse proxy chain has protocol mismatch
- headers contain invalid formatting
- containerized app exits mid-request
To diagnose, I often bypass Nginx and inspect raw behavior with curl -v:
curl -v http://127.0.0.1:3000/
If the response hangs, resets, or returns nonsense, I move straight to the app logs.
For Node.js, that may be:
sudo journalctl -u mynodeapp -n 100 --no-pager
For Python/Gunicorn:
sudo journalctl -u gunicorn -n 100 --no-pager
For Apache behind Nginx:
sudo tail -n 100 /var/log/apache2/error.log
Systemd restart behavior and crash loops
One of the more useful habits I picked up was checking whether the backend is “up” only because systemd keeps restarting it every few seconds.
Inspect the unit:
sudo systemctl status myapp.service --no-pager
Useful details include:
Active:state- recent exit codes
- restart counter
- main PID churn
Then inspect the unit file if needed:
sudo systemctl cat myapp.service
A crash loop often points to application config errors, missing environment variables, bad deploys, or database connectivity failures. Nginx surfaces the result as 502 because the app never stays alive long enough to answer consistently.
A sane unit may include restart handling like this:
[Service]
ExecStart=/usr/bin/gunicorn --workers 4 --bind 127.0.0.1:5000 wsgi:app
Restart=on-failure
RestartSec=3
EnvironmentFile=/etc/myapp/myapp.env
Restart policies are not a fix for broken apps, but they do help the service recover from transient failures.
Common mistakes I see during 502 incidents
Reloading Nginx before testing the config
Always validate first:
sudo nginx -t
Then reload safely:
sudo systemctl reload nginx
Using chmod 777 on sockets or app directories
This is a bad habit. It hides the real permission problem and weakens local security. Fix ownership, service users, and socket modes properly instead.
Increasing timeouts when the upstream is dead
If the log says connection refused or no such file, timeouts are irrelevant.
Ignoring application logs
Nginx does not tell you why a Python worker crashed or why PHP segfaulted. You need the upstream logs for that.
Restarting everything at once
This destroys evidence. I prefer a simple order:
- read Nginx error log
- test upstream directly
- inspect backend service state and logs
- validate configuration
- restart only the affected component
Security considerations during troubleshooting
During outages, people make risky changes fast. I try not to.
- Do not bind internal apps to public interfaces unless you need to.
- Do not loosen socket permissions globally just to make the error disappear.
- Do not disable SELinux or AppArmor as a first step; inspect denials properly.
- Do not expose debug endpoints or stack traces publicly.
- If you switch from Unix socket to TCP for testing, bind to
127.0.0.1, not0.0.0.0.
If you are tightening the host while dealing with proxy issues, Linux server hardening and UFW firewall setup on Ubuntu both complement this work well.
My step-by-step 502 checklist
When I need a repeatable process, this is the one I use:
1. Reproduce and timestamp the error
curl -I https://example.com/
2. Read Nginx error logs immediately
sudo tail -n 50 /var/log/nginx/error.log
3. Test Nginx config syntax
sudo nginx -t
4. Check whether upstream process is running
sudo systemctl status myapp.service --no-pager
sudo ss -ltnp | grep ':5000'
5. Test upstream directly
curl -I http://127.0.0.1:5000/
6. Verify socket or port in Nginx matches reality
sudo nginx -T | grep -n 'proxy_pass\|fastcgi_pass'
7. Check upstream logs
sudo journalctl -u myapp.service -n 100 --no-pager
8. Fix the specific issue, then reload only what changed
sudo systemctl restart myapp.service
sudo systemctl reload nginx
9. Validate again from the client side
curl -I https://example.com/
Conclusion
A 502 Bad Gateway error in Nginx is usually less mysterious than it looks. It is Nginx telling you the upstream side of the architecture failed in a concrete way: wrong port, missing socket, permissions issue, crashed process, invalid response, or a backend that cannot stay alive. The fastest path is to read the Nginx error log, test the upstream directly, and verify the service manager and socket or port configuration match reality. Once you stop treating 502 as a generic web-server error and start treating it as an upstream contract failure, the fix usually becomes much more obvious.