One place for the four nginx errors you actually hit: what the code means, the two to four usual causes, the command that confirms each, and the exact fix. With real error.log lines.

The nginx errors 404, 502, 504 and 413 all look alarming, but each one is a short message about a specific failure, and that message is almost always already in a log. This guide covers all four the same way: what the status code means, the two to four causes that produce it on a normal box, the command that confirms each, and the exact fix. Examples use Ubuntu 24.04 (which ships php-fpm 8.3) and Debian; on other versions substitute yours: service phpX.Y-fpm, paths /etc/php/X.Y/fpm/…, socket /run/php/phpX.Y-fpm.sock. Default PHP is 8.1 on Ubuntu 22.04, 8.2 on Debian 12, 8.4 on Debian 13, and 8.5 on Ubuntu 26.04.

TL;DR. 404: nginx could not find a file at the path built from root / alias, or try_files is missing. Log line: open() ... failed (2: No such file or directory). 502: the upstream (php-fpm, an app) is not listening, or the socket is wrong. Log line: connect() failed (111: Connection refused). 504: the upstream did not answer within the timeout (60 seconds by default). Log line: upstream timed out (110: Connection timed out). 413: the request body is larger than client_max_body_size (1m by default). Log line: client intended to send too large body. Logs live in /var/log/nginx/error.log and /var/log/nginx/access.log; read them with sudo tail -f.

How to read nginx logs

nginx keeps two main logs, and they serve different jobs. access_log gets one line per request: who connected, what they asked for, which status code came back. error_log gets a line only when something broke: the file path, the system error in parentheses, the client address. For 404, 502, 504 and 413 you mostly need error_logaccess_log confirms that the request reached the server and shows the code it returned.

The fastest approach is to follow the log and reproduce the failing request in a browser.

sudo tail -f /var/log/nginx/error.log

  • -f keeps printing new lines until you press Ctrl+C. Keep it in a separate terminal: the relevant line shows up as you trigger the error.

error_log levels, from verbose to rare: debuginfonoticewarnerrorcritalertemerg; the default is error and above, enough for these four. If the log is empty but the problem is real, the affected site usually defines its own log file inside its server block. Check which logs are in effect:

sudo nginx -T | grep -nE 'access_log|error_log'

  • nginx -T prints the full effective configuration, every included file, not just nginx.confgrep -n adds the line number and path.

Plain definitions: reverse proxy, upstream, FastCGI, socket

  • reverse proxy: a server that accepts a request from the internet, hands it to an internal application, and returns the reply. On a website nginx almost always runs in this mode.
  • upstream: the internal application nginx forwards the request to (php-fpm, a Node.js or Python process, another web server).
  • FastCGI: the protocol nginx uses to talk to php-fpm. It is not HTTP, so curl cannot test php-fpm directly.
  • socket: the endpoint nginx connects to, either a network socket (address and port, 127.0.0.1:9000) or a Unix socket, a file such as /run/php/php8.3-fpm.sock.

nginx errors 404, 502, 504 and 413: fast triage table

First a map. Each error is covered in detail below.

Symptom

Most likely

First command

Fix

404 on static files, or on every page except the homepage

Wrong root / alias, or missing try_files

sudo tail -f /var/log/nginx/error.log, then repeat the request

Point root at the real directory; add try_files to location

404 on all domains, or a different site is served

The request is caught by the default server

sudo nginx -T | grep -nE 'listen|server_name'

Check server_name and which block is currently default

403 instead of 404, file is present

No read permission on the file, or no traverse on a parent directory

namei -l /var/www/site/public/index.php

Fix the owner/group or permissions on the specific path segment that's wrong; never 777

502 immediately, no delay

php-fpm or the app is not listening, or the socket is wrong

systemctl status php8.3-fpmsudo ss -xlnp | grep -i php

Start the service; align fastcgi_pass with the pool listen

502 under load, log says no live upstreams

Every server in the upstream group is temporarily marked down (after earlier connection errors)

journalctl -u php8.3-fpm -n 50

Look at earlier connect errors/timeouts; check max_fails / fail_timeout

504 after about 60 seconds

The upstream stopped sending data within the read timeout

grep 'upstream timed out' /var/log/nginx/error.log

Find the slow endpoint with slowlog; raising *_read_timeout is a stop-gap

413 on a file upload or a large POST

client_max_body_size (1m by default)

grep 'too large body' /var/log/nginx/error.log

Raise client_max_body_size and match it against upload_max_filesize / post_max_size, then reload

404 Not Found: nginx could not find the file

One of the most common causes of an nginx 404 is no file at the path nginx computed from root (the site root) plus the request URI, or from alias for a specific location. Less often, nginx returns 404 on purpose, through return 404; or as the last argument of try_files. A quick tell that nginx itself answered, not the app: a bare 404 Not Found page with an nginx line in the footer. It is not proof - a custom error_page or a hidden server_tokens can change that - but it works most of the time.

Four common causes. Wrong root or alias: the path points somewhere other than where the files are. Missing try_files: for WordPress pretty permalinks and single-page apps (SPAs), a request like /about has no matching file on disk, and without try_files nginx never passes it to index.php or index.htmlThe wrong server block catches the request: no server_name matched the host, so nginx used the default server with a different root. Separately, the file exists but the nginx user (usually www-datacannot read it or traverse a parent directory: that returns 403, not 404, but the two get debugged together.

Confirm from the log. Keep tail -f on error.log in another terminal and repeat the request; the line looks roughly like this:

2026/09/03 12:14:02 [error] 5123#5123: *7 open() "/var/www/example.com/public/about"
failed (2: No such file or directory), client: 203.0.113.10, server: example.com,
request: "GET /about HTTP/1.1", host: "example.com"

  • (2: No such file or directory): nothing at that path. Compare the quoted path with where the files really are.
  • (13: Permission denied) in the same shape: the file exists but permissions block it. That is a 403.
  • directory index of "/var/www/example.com/public/" is forbidden: the request hit a directory with no index file and autoindex off. Also a 403.

See which root and try_files actually apply, and whether another block is grabbing the domain:

sudo nginx -T | grep -nE 'root|alias|try_files|server_name'

Check that the path is readable by the nginx user and that every parent directory allows traversal:

namei -l /var/www/example.com/public/index.php

  • namei -l walks the path directory by directory and prints permissions at each level. The nginx user (usually www-data) needs r on the file and x on every parent directory - through the owner, the group, or the "other" class, any of the three works. A missing x at any level, on all three, is the 403.

Fix. Wrong root: set the real directory (the one that holds index.php or index.html), then test and reload:

sudo nginx -t && sudo systemctl reload nginx

Missing try_files: add it to location. For WordPress:

location / {
try_files $uri $uri/ /index.php?$args;
}

For a React or Vue SPA, fall back to index.html:

location / {
try_files $uri /index.html;
}

  • try_files checks each argument in order: is there such a file ($uri), such a directory ($uri/), and if not, it serves the last argument. That routes /about into index.php or index.html, and the app resolves the path.

Wrong block catching the request: check server_name on the intended server block and which block is currently marked default_server. Usually fixing server_name is enough; deliberately making the right site the default server just to catch misdirected Host headers is not a great idea - set up a separate catch-all for that instead:

server {
listen 80 default_server;
server_name _;
return 404;
}

Permissions: for a plain static webroot, this is often enough:

sudo find /var/www/example.com -type d -exec chmod 755 {} \;
sudo find /var/www/example.com -type f -exec chmod 644 {} \;

It is not a universal fix for any site: it rewrites permissions across the whole tree, including config files with sensitive data, directories the app needs to write to, and files that carry special permissions on purpose. More precise: fix the owner, group, or permissions on the exact path segment namei -l flagged, and leave the rest alone. Never 777 either way.

502 Bad Gateway: the upstream refused the connection

A 502 means nginx, acting as a reverse proxy, tried to hand the request to an upstream (php-fpm, a Node.js or Python app, another server) and either could not open the connection or it dropped mid-response. It usually appears instantly.

Causes: the upstream is not running or crashedfastcgi_pass or proxy_pass points at the wrong socket or portthe app is listening, but AppArmor or SELinux blocks nginx from the socketthe app is alive but overwhelmed and rejecting connections (then the log says no live upstreams, when the upstream block has several servers and all are marked down).

The log lines that separate the causes:

connect() failed (111: Connection refused) while connecting to upstream, client: 203.0.113.10,
server: example.com, request: "POST /index.php HTTP/1.1",
upstream: "fastcgi://127.0.0.1:9000", host: "example.com"

  • (111: Connection refused): nothing is listening at that address. The service is down or listening elsewhere.
  • connect() to unix:/run/php/php8.3-fpm.sock failed (2: No such file or directory): the socket file is absent, php-fpm did not start.
  • connect() to unix:/run/php/php8.3-fpm.sock failed (13: Permission denied): the socket file exists, but nginx has no permission to connect to it - see the socket-permission check below.
  • recv() failed (104: Connection reset by peer) while reading response header from upstream: the connection opened, then the upstream cut it. Usually a PHP worker died (out of memory or a fatal error).
  • no live upstreams while connecting to upstream: every server in the upstream group is marked unavailable.

Check the service itself:

systemctl status php8.3-fpm

  • Healthy: active (running)failed or inactive (dead): cause found, details in journalctl -u php8.3-fpm -n 50.

See which socket php-fpm actually listens on, and compare it with fastcgi_pass:

sudo ss -xlnp | grep -i php
sudo nginx -T | grep -nE 'fastcgi_pass|proxy_pass'

  • ss -xlnp lists listening Unix sockets with the owning process: -l listening, -x Unix, -n no name resolution, -p process. The path in the output (/run/php/php8.3-fpm.sock) must match fastcgi_pass. For a TCP upstream, use ss -ltnp | grep 3000.

If the upstream is a plain HTTP app behind proxy_pass, hit it directly, bypassing nginx:

curl -i http://127.0.0.1:3000/

  • A successful response (the code depends on the app) means the upstream is alive and the problem sits between it and nginx. Connection refused means the app is not listening on that address.
  • This does not work for php-fpm: it speaks FastCGI, not HTTP.

If the socket is there and php-fpm is running but the 502 stays, a common cause is that the nginx user (www-data) simply has no permission to connect to the Unix socket. The log then shows (13: Permission denied) instead of (111: Connection refused). Check the socket file's owner, group and mode, and compare with what the php-fpm pool sets:

ls -l /run/php/php8.3-fpm.sock
grep -E '^listen\.(owner|group|mode)' /etc/php/8.3/fpm/pool.d/www.conf

  • For a standard nginx/php-fpm pairing, socket access is normally set through listen.ownerlisten.group and listen.mode in the pool config - that is the first suspect for Permission denied, not a kernel security layer.

If the socket permissions check out and the 502 is still there, look at a kernel security layer - that is the second line of diagnosis, not the first. On Ubuntu and Debian that is AppArmor: look for denials in sudo journalctl -k | grep -iE 'apparmor.*denied' (or /var/log/audit/audit.log if auditd is installed). On SELinux systems (AlmaLinux and relatives), check getenforce, then sudo ausearch -m avc -ts recent and, if needed, sudo setsebool -P httpd_can_network_connect 1.

Fix. Upstream crashed: bring it back and find out why:

sudo systemctl restart php8.3-fpm

Wrong socket: set fastcgi_pass to the pool's listen value (/etc/php/8.3/fpm/pool.d/www.conf, the listen = line), then nginx -t and reload. Overwhelmed under load: check pm.max_children in www.conf; the php-fpm log line server reached pm.max_children setting (5), consider raising it asks for a higher limit directly. First check memory: each PHP worker costs tens of megabytes, and if the server is capped on memory or CPU, extra workers will not help, and the plan may be the limiting factor.

504 Gateway Timeout: the upstream missed the deadline

A 504 Gateway Timeout means nginx did not get what it needed from the upstream at one of two stages: it could not connect within the timeout, or it connected but never got a reply. The default timeout is 60 seconds: fastcgi_read_timeout and proxy_read_timeout are both 60s. One important nuance: this is not a cap on the whole request's runtime - it is the timeout between two consecutive reads of the response. If the upstream keeps sending data every so often (a streamed response, say), the request can legitimately run far longer than 60 seconds without ever hitting a 504 - nginx's own docs say this explicitly.

Causes: the endpoint itself is slow (a heavy report, an export, file generation); a slow database query or an external API call behind the app with no timeout of its own; less often, the nginx timeout is set deliberately low; rarely, the upstream cannot even accept the connection (that is overload, and the log then says while connecting to upstream).

grep 'upstream timed out' /var/log/nginx/error.log | tail

upstream timed out (110: Connection timed out) while reading response header from upstream,
client: 203.0.113.10, server: example.com, request: "GET /export?range=year HTTP/1.1",
upstream: "fastcgi://unix:/run/php/php8.3-fpm.sock", host: "example.com"

  • while reading response header: nginx got the connection but no reply, so the problem is inside the app. The same (110) with while connecting to upstream means it could not even connect: upstream or network overload.
  • The request: field shows the URL that stalls. Start there.

Catch slow PHP requests with slowlog. In /etc/php/8.3/fpm/pool.d/www.conf:

slowlog = /var/log/php8.3-fpm.slow.log
request_slowlog_timeout = 5s

Then sudo systemctl reload php8.3-fpm, reproduce the page, and read the log: it holds a PHP stack trace frozen on a specific function (a database query, a curl call to an API).

Fix. The real fix is to make the endpoint faster: an index for the query, pagination instead of dumping everything at once, heavy work moved to a background queue, a hard timeout on external calls. As a stop-gap, raise the timeout in the relevant location only, not globally:

location ~ \.php$ {
fastcgi_read_timeout 120s;
}

For proxy_pass, use proxy_read_timeout 120s;. Then nginx -t and reload. The request still hangs for two minutes instead of one: this is a patch, not a cure.

413 Request Entity Too Large: the request is over the limit

A 413 means the request body (an uploaded file, a large POST) went over client_max_body_size. The default is 1m, so the first file heavier than a megabyte hits the wall. Browsers often render this error badly or just abort the upload.

grep 'too large body' /var/log/nginx/error.log | tail

client intended to send too large body: 8388608 bytes, client: 203.0.113.10,
server: example.com, request: "POST /wp-admin/async-upload.php HTTP/1.1", host: "example.com"

  • The byte count is how much the client tried to send. 8388608 is 8 MB; set the limit with headroom above that.

Fix. Raise the limit in nginx. The directive can go in http (whole server), server (one site), or location (only the upload path); a value from a narrower context overrides a wider one.

server {
client_max_body_size 50m;
}

Then sudo nginx -t && sudo systemctl reload nginx: reload, not restart.

nginx alone is not enough: PHP has its own limits, and without them the wall just moves one layer down. In /etc/php/8.3/fpm/php.ini, if you want to accept files up to 50 MB:

upload_max_filesize = 50M
post_max_size = 55M

  • upload_max_filesize is the cap on a single file; post_max_size is the cap on the whole POST body, and per the PHP docs it must be greater than (not just at least) upload_max_filesize - the POST body carries the multipart overhead and the other form fields on top of the file itself. Give it some headroom, say 5-10% above. There is no formal rule that post_max_size must be at least client_max_body_size - just keep both numbers consistent with the real maximum upload you want to allow. After editing, run sudo systemctl reload php8.3-fpm.

If nginx sits in front of a Node.js or Python app instead of PHP, that framework has its own body-size limit to raise too.

FAQ

How do I know a 404 came from nginx and not from WordPress or a framework?

Look at the page. Bare 404 Not Found text with an nginx line in the footer is nginx: the request never reached the app. A styled page with the site header means the app returned the 404. In the nginx log the first case shows as open() ... failed (2: No such file or directory).

502 or 504: what is the difference?

502 means nginx could not hand the request to the upstream: the connection was refused, dropped, or the service is not listening; it usually appears at once. 504 means nginx connected (or tried to) but did not hear back at that stage within the timeout (60 seconds by default). In the log that is connect() failed versus upstream timed out.

Where should client_max_body_size go: httpserver, or location?

Any of the three. In http it is server-wide, in server it applies to one site, in location only to a specific path such as an upload form. A value from a narrower context overrides a wider one, so keep the global limit small and raise it where needed.

I raised client_max_body_size and the 413 is still there.

The limit moved to the PHP layer. Raise upload_max_filesize and post_max_size in php.ini, keeping post_max_size greater than upload_max_filesize (the POST body is bigger than just the file), then run sudo systemctl reload php8.3-fpm. For a Node.js or Python app, check the limit in its framework.

Does raising the timeout fix a 504?

As a stop-gap, yes: the page stops returning 504. But the request still runs long and the user still waits. In parallel, find the slow endpoint with slowlog and remove the cause: a database index, pagination, a background queue.

Do I need to restart nginx after editing the config?

No, a reload is enough: sudo nginx -t && sudo systemctl reload nginx. It applies the new config without dropping current connections. PHP settings apply with sudo systemctl reload php8.3-fpm.

The logs are empty but the error is real. Why?

You are probably reading the wrong file: a site may set its own error_log and access_log inside its server block. Less often, the error_log level is higher than the event. Check the paths with sudo nginx -T | grep -nE 'error_log|access_log'.

Can a 502 or 504 come from the server running out of resources?

Yes. If the app runs out of memory and the OOM killer takes it, nginx sees a refused connection (502). If the CPU is saturated and responses come slowly, you get 504. Then the nginx config is not the problem, and if the server cannot carry the load the plan may be the limiting factor.

Takeaways

  • All four errors are readable in /var/log/nginx/error.log. Run sudo tail -f and repeat the request.
  • 404: nginx found no file via root / alias, or try_files is missing. Line: open() ... failed (2: No such file or directory). File-read and directory-traverse permissions cause 403; check with namei -l.
  • 502: the upstream is not listening or the socket is wrong. Line: connect() failed (111: Connection refused). Check systemctl status php8.3-fpm and ss -xlnp.
  • 504: the upstream stopped sending data within the read timeout (60 seconds between reads by default, not a cap on the whole request). Line: upstream timed out (110: Connection timed out). Find the slow endpoint with slowlog; raising the timeout is temporary.
  • 413: the request body is over client_max_body_size (1m by default). Raise it together with upload_max_filesize and post_max_size, then reload both services.
  • nginx config applies with nginx -t && systemctl reload nginx, no restart.