# HTTP 404 Not Found: What It Means and How to Fix It

> 404 Not Found means no resource exists at that URL. Find which layer sent it, then fix routing, deploys, rewrites, CDN cache and soft 404s, with curl checks.

Source: https://howhttpworks.com/status-codes/404
Last reviewed: 2026-10-04

> **TL;DR:** 404 Not Found means the server has no resource at that URL, or does not want to say whether it has one. Visitors should check the URL for typos; site owners should check routing, deploy output, rewrite rules and CDN cache, and find out which layer generated the 404.

RFC 9110 section 15.5.5: the origin server "did not find a current representation for the target resource or is not willing to disclose that one exists". A 404 is heuristically cacheable and gives no hint whether the condition is permanent; use [410 Gone](https://howhttpworks.com/status-codes/410) when it is.

```http
HTTP/1.1 404 Not Found
Server: nginx/1.27.1
Content-Type: text/html

<html><head><title>404 Not Found</title></head>
<body><center><h1>404 Not Found</h1></center><hr><center>nginx</center></body></html>
```

## If you are a visitor

Check the address for typos and a trailing character pasted from a chat app; remove path segments one at a time to find a parent page; use the site search; or look the page up on the Wayback Machine. A page that worked yesterday was moved or deleted, and only the site owner can restore it.

## Who sent it?

| Evidence | Layer |
| --- | --- |
| nginx error log: `open() "/var/www/site/missing.html" failed (2: No such file or directory)` | nginx static file lookup |
| Apache: `AH00128: File does not exist: /var/www/html/missing` | Apache |
| `Cannot GET /path` in the body | Express, no matching route |
| `This page could not be found.` (and `404 \| This page could not be found`) | Next.js default not-found page |
| `Not Found: /path` or "Using the URLconf defined in ..., Django tried these URL patterns" | Django (the latter only with `DEBUG = True`) |
| `Whitelabel Error Page`, `"status":404` JSON with `"error":"Not Found"` | Spring Boot |
| `x-vercel-error: NOT_FOUND`, `404: NOT_FOUND Code: NOT_FOUND` | Vercel |
| `<Code>NoSuchKey</Code>` | S3 object missing (private buckets return 403 instead when you lack `s3:ListBucket`) |
| `X-Cache: Error from cloudfront` | CloudFront passing through an origin 404 or a missing default root object |
| `404 Not Found` plus `CF-Ray` and a custom page | Cloudflare: check whether the origin or a Worker or Page Rule produced it (`cf-cache-status` tells you if it was a cached 404) |

## Fix it, most common first

1. **The URL is wrong.** Case matters on Linux paths (`/About` is not `/about`), as do trailing slashes in some routers.
2. **The file never deployed.** List the build output on the server or bucket. Confirm the deploy finished and that all regions or pods run the same release; a rolling deploy makes some requests 404 and others succeed.
3. **Root path mismatch.** nginx `root` vs `alias`: with `location /static/ { alias /srv/assets/; }` the trailing slashes must match, whereas `root` appends the whole URI.
4. **Single-page app without fallback.** Deep links like `/dashboard/settings` 404 on refresh because the server looks for a file. Route unknown paths to `index.html`.
5. **Rewrite or proxy path error.** `proxy_pass http://app;` and `proxy_pass http://app/;` treat the path differently: a trailing slash replaces the matched location prefix, none passes the URI unchanged.
6. **Stale CDN cache.** A 404 cached before the file was published keeps being served. Purge the URL, and set a short TTL for 404s (Cloudflare caches 404s for a few minutes by default; CloudFront's error caching minimum TTL defaults to 10 seconds).
7. **Works with a trailing slash, 404 without (or the reverse).** Static hosts map `/docs` to `/docs/index.html` only with directory-style output; check your framework's `trailingSlash` setting.
8. **WordPress.** Permalinks 404 after migration: Settings > Permalinks > Save, and confirm `mod_rewrite`/`AllowOverride All` or the nginx `try_files $uri $uri/ /index.php?$args;`.

## Fix by stack

### nginx

```nginx
location / {
    try_files $uri $uri/ /index.html;       # SPA fallback; use /index.php?$args for WordPress
}

error_page 404 /404.html;
location = /404.html { internal; }          # keeps the 404 status; do not redirect to it
```

### Apache

```apache
ErrorDocument 404 /404.html                 # a local path keeps the 404 status; a full URL turns it into a 302
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ /index.html [L]
```

### Next.js

```typescript
// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation'

export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params
  const post = await getPost(slug)
  if (!post) notFound()          // renders app/not-found.tsx with status 404
  return <article>{post.title}</article>
}
```

Do not render a "not found" message with status 200: that is a soft 404, which Google reports in Search Console as "Soft 404".

### Express

```javascript
app.get('/api/users/:id', async (req, res) => {
  const user = await db.users.findById(req.params.id)
  if (!user) return res.status(404).json({ error: 'not_found' })
  res.json(user)
})

app.use((req, res) => res.status(404).json({ error: 'not_found', path: req.path }))  // last
```

### Cloudflare and static hosts

Cloudflare Pages serves `404.html` for missing paths (and does SPA fallback if there is no `404.html`). Netlify uses `404.html` or a `_redirects` rule `/*  /index.html  200`. Vercel uses `vercel.json` `rewrites` for SPA fallback.

## Reproduce and verify

```bash
curl -sI https://example.com/missing | head -n 1                       # HTTP/2 404
curl -s -o /dev/null -w '%{http_code}\n' https://example.com/page      # status only
curl -sIL https://example.com/old-url | grep -iE '^(HTTP|location)'    # follows redirects
curl -sI https://example.com/missing | grep -iE '^(server|via|cf-ray|x-cache|age|cf-cache-status)'
```

An `Age` or `cf-cache-status: HIT` on a 404 means the CDN is serving a cached miss. The [redirect audit](https://howhttpworks.com/tools/redirect-audit) shows whole chains.

## SEO

Google drops 404 URLs from the index after recrawling; a few are normal. Redirect moved pages with a [301](https://howhttpworks.com/status-codes/301) to the closest equivalent page, return [410](https://howhttpworks.com/status-codes/410) for deliberately removed content, and never redirect everything to the homepage (Google treats that as a soft 404). Fix internal links that point to 404s; they waste crawl budget and frustrate users.

## 404 vs its neighbors

[400](https://howhttpworks.com/status-codes/400) is a malformed request; [403](https://howhttpworks.com/status-codes/403) is a refusal (sometimes disguised as 404 to hide existence); [405](https://howhttpworks.com/status-codes/405) means the path exists but not for that method; 410 means gone for good. API design: 404 for a missing resource, 400 or 422 for a bad body, and never 200 with `{"error":"not found"}`.

See also the comparison [403 vs 404](https://howhttpworks.com/compare/403-vs-404): when to hide a resource behind a 404 instead of admitting it with a 403.
