404
The server understood the request and has nothing to serve at that path — which on a modern stack is far more often a routing or deployment mistake than a dead link.
The same three digits are three different problems depending on which side of the request you are on. Read the block that describes you.
Why you are seeing it
Three shapes account for almost all of them. A single-page route works when you click to it and 404s when you refresh or open it in a new tab, because the click was handled by the history API while the reload was a real HTTP request the server has no rule for. An asset 404s immediately after a deploy, because a cached `index.html` is still asking for the previous build's hashed filename. Or a `fetch` 404s while the same URL works in the address bar, which usually means the request went somewhere else entirely — a different origin, a proxy prefix that was added or stripped, or a relative path that resolved against the current route instead of the site root.
What to do
Read the request URL in the network panel rather than the one in your source, because the difference between the two is the bug: a leading slash, a doubled `/api/api/`, a trailing slash your router treats as a separate route, or a host you did not mean to call. For the refresh case the server needs a history-API fallback that serves `index.html` for unmatched paths — `try_files $uri /index.html` in nginx, or the equivalent rewrite on your host — and a static export needs to agree with the host about whether URLs end in a slash. For the stale asset, make the HTML itself uncacheable while the hashed files stay immutable, so a browser can never hold an old document pointing at files that no longer exist.
Why you are seeing it
Either your router has no match for that method and path pair, or something in front of it rewrote the path before your application ever saw it. From outside they look identical, and the reverse proxy is the usual culprit: whether a `location /api/` block's `proxy_pass` ends in a slash decides whether the prefix is stripped, so the application sees `/users` or `/api/users` depending on one character. Case and trailing slashes are the other two — URL paths are case-sensitive on a Linux origin, and most routers treat `/users` and `/users/` as different routes unless configured otherwise.
What to do
Ask the application which routes it actually registered — every framework has a route-list command — and compare that against the exact path in the access log, not the path in your source. Then take the proxy out of the picture: `curl --resolve example.com:443:203.0.113.10 https://example.com/api/users` connects straight to the origin address while still sending the original host name and SNI, so a 404 that survives is your application and a 404 that disappears is your proxy. Check the origin's access log as well, because a request that never appears there never arrived — which points at DNS, at the wrong virtual host, or at a default server quietly catching a host name your configuration does not cover.
Why you are seeing it
The address exists as a link but not as a page. Either it was typed or copied with a character missing, or the page really has been moved or deleted, or the link came from an old email, bookmark or search result and the site has been reorganised since. Search engines keep pages in their index for a while after they disappear, so a search result that leads to a 404 is ordinary rather than a sign that something is wrong with your device or your connection.
What to do
Look at the address bar first — a missing letter, a stray space pasted from a chat message, or a URL broken across two lines in an email accounts for most of them. Then cut the address back one segment at a time, so `/blog/2024/some-post` becomes `/blog/2024/` and then `/blog/`, until you reach a page that loads, and navigate from there. The site's own search will usually find a moved page faster than a search engine can. If the page genuinely is gone and you still need what was on it, the Internet Archive's Wayback Machine often has a copy of the old version.
Run this against the URL that failed. It prints the status without the error page, so you can see what the server said rather than what the browser rendered.
curl -sS -o /dev/null -L -w '%{http_code} %{num_redirects} %{url_effective}\n' https://example.com/no-such-page`-sS` drops the progress meter but keeps error messages, `-o /dev/null` throws the error page body away, `-L` follows any `Location` headers, and `-w` prints the final status, how many redirects were followed, and the URL that produced that status. The last field is the point of the command: curl documents `%{url_effective}` as most meaningful once you have asked it to follow redirects, and a 404 at the end of a redirect chain is a different bug from a 404 on the URL you typed — it means a rewrite sent you somewhere that does not exist. A `%{num_redirects}` above zero on a URL you expected to be direct is the tell. To find out whether the edge or the origin produced the 404, run the request twice: once normally, and once with `--resolve example.com:443:203.0.113.10`, which makes curl connect to the address you name while still sending the original host name and SNI. Two different answers mean the edge is serving something the origin is not, which for this code is often a cached 404.
RFC 9110 §15.5.5 defines 404 as the origin server not finding a current representation for the target resource, or not being willing to disclose that one exists. Both halves are load-bearing. The first means 404 is about this URL at this moment and says nothing about whether the resource ever existed or ever will — the same section points at 410 (Gone) as the better answer when the server knows the condition is permanent. The second is why 404 and 403 overlap: §15.5.4 explicitly allows an origin server that wants to hide the existence of a forbidden resource to answer 404 instead, so a 404 on a URL you are certain exists can be an access decision wearing a disguise. One more property catches people mid-incident. 404 is one of the status codes RFC 9110 §15.1 lists as heuristically cacheable, which means a proxy or CDN may cache it with no explicit `Cache-Control` at all, so a URL that 404s during a bad deploy can keep 404ing after the deploy is fixed, until the stored response expires or somebody purges it. And 404 carries no opinion about the method: a POST to a path that only accepts GET is 405, not 404, and a framework that answers 404 there is choosing not to tell you which half was wrong.
| Confused with | How to tell them apart |
|---|---|
| 403 | 403 says the server understood and refuses; 404 says it has nothing there. RFC 9110 §15.5.4 blurs the two deliberately — an origin server that does not want to admit a forbidden resource exists is explicitly permitted to answer 404 instead. So a 404 on a URL you know exists is worth retrying with credentials before you go hunting for a routing bug, because object storage buckets and private repositories do exactly this. |
410 | 410 (Gone) is the answer when the server knows the resource is permanently finished, and RFC 9110 says it is preferred over 404 in that case. The difference is a signal to machines rather than to people: a 404 invites retries, while a 410 tells an indexer to stop asking. If you are deliberately retiring content, 410 is the more truthful code and the faster one to get out of a search index. |
| 405 | 405 (Method Not Allowed) means the path exists but not for the verb you used, and RFC 9110 requires the response to carry an `Allow` header listing the methods that do work. A framework that answers 404 for a POST to a GET-only route is permitted to, but it is hiding the useful half of the answer — so check the method before you start doubting the path. |
soft 404 | A soft 404 is a page whose body apologises for being missing while the status line says 200. Nothing in HTTP forbids it, but search engines then index an error page as content, and scripts see a success status and parse the apology as data. If a page you know is missing does not produce a 404 under curl, that mismatch is itself the bug. |
nginx | nginx produces 404 from more places than a missing file. A `try_files` chain ending in `=404` returns one deliberately; a request whose `Host` matches no `server_name` lands on the default server, which is often a stub with an empty root; and an `alias` that lost its trailing slash builds a filesystem path one directory short. The error log records the path it actually tried, which is the quickest way to tell the three apart. |
All of these run in your browser — nothing is uploaded.
Codes people usually end up reading in the same session as 404.
Read the response headers rather than the page. curl -sS -o /dev/null -D - https://example.com/path prints them without the body, and server, via and cf-ray between them name the layer that answered. A cf-ray value means Cloudflare handled the response and is the id their support will ask for. To take the edge out of the picture entirely, repeat the request with --resolve example.com:443:203.0.113.10, which connects to the origin address you name while still sending the original host name and SNI — if the answer changes, the edge and the origin disagree.
No, and you should never branch on it. RFC 9112 tells clients to ignore the reason phrase, servers are free to change it, and HTTP/2 and HTTP/3 do not carry one at all — so 404 Not Found over HTTP/1.1 arrives as a bare 404 over HTTP/2. The phrases on this site are the registered ones because they are what people search for and what appears in an HTTP/1.1 log, not because any software depends on them.
Retry 5xx and 429; do not retry 4xx, because nothing about the request will be different next time. If the response carries Retry-After, honour it — RFC 9110 defines it for exactly this, and it may be either a number of seconds or a date. Otherwise use exponential backoff with jitter and a hard cap, and only for idempotent methods: a retried POST can charge a card twice. A retry storm against a server that is already failing is how a brief incident becomes a long one.
Yes, and the only rule that matters is that the code has to be true. Anything that reads your responses automatically — search engines, monitoring, caches, client retry logic — makes decisions from the number and never from the page. Serving an error page with 200 hides the failure from your own alerting; serving 200 for a missing page gets the error indexed as content; returning 500 for a request that was simply malformed sends whoever is on call to the wrong half of the stack.
Still stuck on 404? browse every status code in the reference — or go back up to the block written for your side of the stack.