What 404 actually means
A 404 is a successful conversation with a server that has nothing at that address. The DNS lookup worked, the connection worked, TLS worked, and the server answered: “I don’t have anything for this path.”
That rules a lot out. A DNS problem gives you ERR_NAME_NOT_RESOLVED in the browser, a dead server gives you ERR_CONNECTION_REFUSED or a timeout, and neither has an HTTP status at all. A 404 always comes from something that speaks HTTP.
The status also says nothing about whether the absence is permanent. That’s what 410 Gone is for, and it matters for deleted pages (see below).
If you’re a visitor
Check the URL for typos first, including capital letters: on most servers /About and /about are different paths. Remove anything after the last meaningful part of the path, or go up one level to the section page. If a page was deleted, an archive such as the Wayback Machine may still have a copy. Beyond that the fix is on the site’s side; the owner might want to know which link sent you there.
Step 1: find out who said 404
“404” is produced by many different layers, and each one points at a different fix. The body of the response usually gives it away:
curl -sI https://www.example.com/missing
curl -s https://www.example.com/missing | head -20
| What you see | Who answered |
|---|---|
| “404 Not Found” with “nginx” underneath | nginx found no file and no matching location handled it |
| “Not Found. The requested URL was not found on this server.” | Apache |
Cannot GET /path | An Express (Node) app with no route for that method and path |
| “Whitelabel Error Page” | A Spring Boot app with no handler for the path |
Plain 404 page not found | A Go server’s default handler, including Traefik when no router matches |
{"message":"no Route matched with those values"} | Kong API gateway: no route for that host/path |
404: NOT_FOUND with an ID | Vercel |
XML with <Code>NoSuchKey</Code> | Amazon S3 (or an S3-compatible store) |
| “There isn’t a GitHub Pages site here.” | GitHub Pages, for a custom domain it doesn’t know |
| Your own site’s 404 design | Your application’s router |
A “no route” message from a gateway, ingress or proxy is the important one to recognise: the request was stopped before it reached your code, so no amount of debugging the app will help. Look at the gateway’s route definitions for that host and path instead.
The HTTP check shows the full redirect chain and the final status from outside your network. Look at it before anything else: a surprising number of 404s happen at the end of a redirect that sent the request somewhere it shouldn’t go.
The whole site is 404, including the homepage
If even / returns 404, the request is reaching the wrong place, or the right place has no content.
The wrong virtual host answered. A web server picks a site by the Host header. If your hostname isn’t in any server_name (nginx) or ServerName/ServerAlias (Apache), the default site answers, and it often has nothing at your paths. Compare:
curl -sI -H 'Host: www.example.com' http://203.0.113.10/
curl -sI -H 'Host: example.com' http://203.0.113.10/
nginx -T | grep -n server_name
If one name works and the other gives 404, add the missing name to the right server block. www and the bare domain are separate names and both need to be listed.
DNS points somewhere else. After a migration, a record may still point at the old host, which no longer serves this site. Check where the name resolves with the DNS lookup and compare with where you deployed.
The document root is wrong or the build output is empty. root points at /var/www/site but the build went to /var/www/site/dist; a static host is configured to publish build/ while the framework writes dist/. For nginx, a missing directory gives 404, while an existing directory with no index file and autoindex off gives 403 instead, which is a useful distinction.
The platform doesn’t know your custom domain. GitHub Pages, S3 website endpoints, Netlify and similar return their own 404 when the domain points at them but isn’t attached to a site in their settings.
Only some paths are 404
Refresh gives 404 in a single-page app
The classic. Clicking around a React, Vue or Angular app works, but reloading /dashboard/settings or opening it from a link gives 404. The routes exist only in JavaScript; the server looks for a file called dashboard/settings and finds nothing.
Tell the server to hand unknown paths to index.html:
location / {
try_files $uri $uri/ /index.html;
}
On Apache, FallbackResource /index.html. On Netlify, a _redirects line /* /index.html 200. Other static hosts have an equivalent rewrite setting.
The fallback has a cost: now every unknown URL returns 200 with your app shell. That’s a soft 404 (see below), so make the app’s not-found view do something about it.
Case sensitivity
Linux file systems are case-sensitive, Windows and macOS (by default) aren’t. A link to /images/Logo.png works on your laptop and 404s on the server where the file is logo.png.
Git adds a trap: on a case-insensitive file system, renaming Logo.png to logo.png may not register as a change. Use git mv -f Logo.png logo.png so the rename actually reaches the repository.
Trailing slashes and base paths
/docs and /docs/ are different URLs. Static hosts usually serve /docs/ from docs/index.html, and whether /docs works depends on the host and on your framework’s trailing-slash setting. Pick one form, generate links in that form, and redirect the other.
Reverse proxies have a subtler version. In nginx, whether proxy_pass has a URI part changes what the backend receives:
location /api/ {
proxy_pass http://backend:3000/; # /api/users -> /users
}
location /api/ {
proxy_pass http://backend:3000; # /api/users -> /api/users
}
If the backend’s routes are /users and you used the second form (or the reverse), every API call 404s. The same applies to apps deployed under a sub-path: if the app is served at /app/ but its assets are referenced as /assets/..., the page loads and every script and image 404s. Set the framework’s base path.
Rewrite rules that aren’t running
WordPress pretty permalinks, Laravel routes and most PHP frameworks depend on a rewrite in .htaccess. Two ways it silently doesn’t run:
AllowOverride Nonefor that directory, so Apache ignores.htaccessentirely. The homepage works, every other page 404s.- mod_rewrite isn’t loaded, and the rules are wrapped in
<IfModule mod_rewrite.c>, so instead of an error they’re skipped.
Both give the same symptom: / works and /about doesn’t. Check apachectl -M | grep rewrite and the AllowOverride setting in the site’s <Directory> block. On nginx there’s no .htaccess at all, so the rewrite has to be in the server config (for PHP frameworks usually try_files $uri $uri/ /index.php?$query_string;).
File names with non-ASCII characters
A file named in Korean or with spaces is requested in percent-encoded form, and the server has to decode it to the exact bytes on disk. Files created on macOS can end up stored with Unicode in decomposed form (NFD), while links are almost always written in composed form (NFC). They look identical and don’t match, so the file 404s. Renaming the file on the server, or sticking to ASCII file names, avoids it.
Deleted pages: 301, 404 or 410
When you remove or move a page, choose deliberately:
- The content moved or has a clear replacement: 301 to the new URL. Visitors and links follow, and search engines transfer the old URL’s standing to the new one over time. See 301 vs 302.
- The content is gone for good: return 404 or 410. Google has said it handles them almost the same way; 410 states the removal is intentional and may get the URL dropped a little faster.
- Don’t redirect everything to the homepage. A visitor looking for a specific article gets a page that doesn’t answer them, and Google may treat a redirect to an unrelated page as a soft 404 anyway.
404s on URLs that should not exist don’t hurt the rest of your site in search. What’s worth fixing is 404s on URLs that people or other sites still link to. Search Console’s page indexing report lists them, and your access log does too:
awk '$9 == 404 {print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head -20
(That assumes the default combined log format, where field 9 is the status and field 7 is the path.)
Soft 404s: the opposite problem
A soft 404 is a page that says “not found” but returns status 200. Search engines notice the mismatch, flag it as a soft 404 and may drop it, and your monitoring sees nothing wrong.
Common sources:
- An SPA fallback that returns 200 for every path, with the not-found message rendered by JavaScript.
- A custom error page configured as a redirect. Apache’s
ErrorDocument 404 /404.htmlkeeps the 404 status;ErrorDocument 404 https://example.com/404.html(a full URL) makes Apache redirect instead, so the final response is a 200. - Empty search results or empty category pages that look like errors.
For server-rendered sites, make sure your custom error page goes out with status 404: nginx’s error_page 404 /404.html; does that by default. For SPAs, either have the server know which routes exist and return 404 for the rest, or have the not-found view add <meta name="robots" content="noindex">. Verify with curl -sI rather than with a browser, since the page looks the same either way.
After the fix
Run the HTTP check on the URL that failed and on a couple of neighbours: the www and bare-domain versions, with and without a trailing slash. The final status should be 200 for pages that exist, and still 404 for a nonsense path, which proves you haven’t created a soft 404 on the way.
Related guides
- 403 Forbidden: the path exists (or might), but access is refused.
- Redirect loop diagnosis: when the redirects never end instead of ending in a 404.
Check your own domain now
Free, no sign-up. Runs the exact check this guide describes and shows what to fix.