Nginx returns 502 Bad Gateway when it acts as a proxy and the app behind it gives no valid answer: the app is down, listening on another port or socket, or crashed mid-reply. Start the app, make proxy_pass point at the address it really listens on, then read the nginx error log for the exact reason.
| Status | What nginx is telling you | First place to look |
|---|---|---|
| 502 Bad Gateway | The upstream app sent nothing usable | Is the app running on the proxy_pass address? |
| 504 Gateway Timeout | The upstream app was too slow | proxy_read_timeout (default 60s) |
| 403 Forbidden | Nginx refuses to serve the path | Missing index file, file permissions, deny rules |
| 404 Not Found | No file at the path nginx built | root versus alias, try_files |
| 413 Request Entity Too Large | The upload body is over the limit | client_max_body_size (default 1m) |
Updated
Pick the status code you see, or type a word from your error log, and the finder narrows the list to the matching causes. Each card names the cause, the check that confirms it, and a config snippet you can adapt. Every card is shown until you filter.
Status code
Words from your error log
11 matching causes
Check: Curl the proxy_pass address from the nginx machine. A refused connection confirms it.
Fix: Start the app under a process manager that restarts it after a crash.
# confirm from the nginx host
curl -i http://127.0.0.1:3000/Check: Compare the address in proxy_pass with the one the app prints when it starts.
Fix: Make the two match exactly, including the host part.
location / {
proxy_pass http://127.0.0.1:3000;
}Check: Look for a log line that mentions the header being too big.
Fix: Raise proxy_buffer_size for that location. Its default is one memory page, 4k or 8k.
location / {
proxy_pass http://127.0.0.1:3000;
proxy_buffer_size 16k;
proxy_buffers 8 16k;
}Check: Time the same request against the app directly with curl.
Fix: Speed up the endpoint or move the work to a background job. Raise the timeout only for that location.
location /reports/ {
proxy_pass http://127.0.0.1:3000;
proxy_read_timeout 180s;
}Check: Check for a firewall or a host that is unreachable from the proxy.
Fix: Fix the network path. The nginx docs note this timeout cannot usually exceed 75 seconds, so raising it does not help.
proxy_connect_timeout 10s;Check: List the folder that root points at and look for index.html.
Fix: Add the index file, or send the request to your app with try_files.
location / {
root /var/www/site;
index index.html;
try_files $uri $uri/ /index.html;
}Check: Look for a permission denied line in the error log naming the path.
Fix: Give the worker user read access to the files and execute access to every parent folder.
namei -l /var/www/site/index.htmlCheck: Read every allow and deny line that applies to the location, top to bottom.
Fix: Reorder the rules so the specific allow comes before the broad deny all.
location /admin/ {
allow 203.0.113.0/24;
deny all;
}Check: Work out the file path: root appends the whole URI, alias replaces the matched prefix.
Fix: Use alias, with a trailing slash on both sides, when the folder name differs from the URL.
location /images/ {
alias /data/uploads/;
}Check: The home page loads but a refresh on any inner route fails.
Fix: Fall back to index.html with try_files so the app's router handles the path.
location / {
try_files $uri $uri/ /index.html;
}Check: Uploads under a megabyte work and larger ones fail.
Fix: Raise the limit for the upload location only.
location /upload {
client_max_body_size 25m;
proxy_pass http://127.0.0.1:3000;
}A starting point for an app on port 3000. It passes the original host and scheme through, sets explicit timeouts and leaves room for larger headers.
Test with nginx -t, then reload. Adjust the port and the limits to your app.
server {
listen 80;
server_name example.com;
client_max_body_size 10m;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_connect_timeout 10s;
proxy_read_timeout 60s;
proxy_buffer_size 16k;
proxy_buffers 8 16k;
}
}
In most setups nginx sits in front of another program: a Node server, PHP-FPM, a Python app. Nginx forwards the request and relays the reply. When that program is missing, slow or broken, nginx is the one that has to answer the browser, so the error carries nginx's name even though the fault sits one hop back.
That is why 502 and 504 are gateway codes. They describe what happened between the proxy and the app, not between the browser and the proxy.
The browser only shows a status code. The nginx error log records why: a refused connection, a timeout, a missing file, a permissions failure. Matching on that reason saves you from raising timeouts when the app simply is not running.
As a visitor, nothing you do causes it. The proxy reached for the application behind it and got no valid reply. As the site owner it is your app or your proxy settings, so start by checking that the upstream process is running.
A 502 means the upstream answered badly or refused the connection. A 504 means nginx waited and nothing arrived in time. The first points at a stopped or crashing app, the second at slow work or a timeout set too low.
Set client_max_body_size in the http, server or location block, for example client_max_body_size 20m;. The default is 1m. A value of 0 turns the size check off, which is rarely what you want on a public site.
When the URL ends in a slash, nginx looks for an index file such as index.html. If none exists and autoindex is off, which is the default, there is nothing it may serve. Add the index file or route the request with try_files.
Its location is set by the error_log directive in your config. Search your nginx.conf and included files for error_log to find it, since packages and containers place it differently.
Related: HTTP status codes reference, Fix too many redirects, HTTP 405 Method Not Allowed, 401 vs 403, Redirect checker.
Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.
We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.