How HTTP Works
4xx · Client error
< HTTP/1.1 404 Not Found

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.

Reviewed 5 min readbeginner5 sourcesTry itMarkdown
Cacheable
Yes, by default
Retry?
Not unless the resource gets created
Usually sent by
Origin app or static host
Spec
RFC 9110 §15.5.5
Often confused with
410, 403
On this page

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 when it is.

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?

EvidenceLayer
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/missingApache
Cannot GET /path in the bodyExpress, 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_FOUNDVercel
<Code>NoSuchKey</Code>S3 object missing (private buckets return 403 instead when you lack s3:ListBucket)
X-Cache: Error from cloudfrontCloudFront passing through an origin 404 or a missing default root object
404 Not Found plus CF-Ray and a custom pageCloudflare: 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

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

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

// 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

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

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 shows whole chains.

SEO

Google drops 404 URLs from the index after recrawling; a few are normal. Redirect moved pages with a 301 to the closest equivalent page, return 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 is a malformed request; 403 is a refusal (sometimes disguised as 404 to hide existence); 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: when to hide a resource behind a 404 instead of admitting it with a 403.

In practice

Express.js
// Return 404 for missing resources
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',
      message: `User ${req.params.id} not found`
    })
  }
  res.json(user)
})

// Catch-all 404 handler (must be last)
app.use((req, res) => {
  res.status(404).json({ error: 'not_found', path: req.path })
})
Next.js App Router
// app/api/users/[id]/route.ts
import { notFound } from 'next/navigation'

export async function GET(
  _req: Request,
  { params }: { params: Promise<{ id: string }> }
) {
  const { id } = await params   // params is a Promise in Next.js 15+
  const user = await db.users.findById(id)
  if (!user) {
    // For API routes, return 404 response directly
    return new Response(
      JSON.stringify({ error: 'not_found' }),
      { status: 404, headers: { 'Content-Type': 'application/json' } }
    )
  }
  return Response.json(user)
}

// For page routes, call notFound() to render the not-found.tsx page
Django
from django.http import JsonResponse
from django.shortcuts import get_object_or_404

# Option 1: get_object_or_404 raises Http404 automatically
def user_detail(request, user_id):
    user = get_object_or_404(User, pk=user_id)
    return JsonResponse(user.to_dict())

# Option 2: Manual 404 for APIs
def user_detail_api(request, user_id):
    try:
        user = User.objects.get(pk=user_id)
        return JsonResponse(user.to_dict())
    except User.DoesNotExist:
        return JsonResponse({'error': 'not_found'}, status=404)

Frequently asked questions

What does 404 Not Found mean?

The server found no current representation for the target URL, or does not want to reveal that one exists (RFC 9110 section 15.5.5). It says nothing about whether the condition is permanent; 410 Gone is the permanent version.

How do I fix a 404 error on my website?

Confirm the file or route exists in the deployed build, check case and trailing slashes, verify nginx root vs alias and proxy_pass paths, add an SPA fallback if deep links fail, and purge CDN cache if a cached 404 persists.

What is the difference between 404 and 410?

404 means not found with no statement about permanence, while 410 Gone says the resource was deliberately and permanently removed. Google treats both as removals, with 410 sometimes processed slightly faster.

Do 404 errors hurt SEO?

A normal number of 404s does not harm rankings, because they are the correct response for missing pages. Broken internal links, mass 404s after a migration without 301s, and soft 404s (a 200 page saying not found) do cause problems.

Why do I get intermittent 404 errors on a page that exists?

Typical causes are a rolling deploy where some servers lack the new files, a CDN or edge that cached an earlier 404, and a load balancer routing to a stale origin. Compare responses across origins with curl and check the Age or cf-cache-status header.

What is a soft 404?

A soft 404 is a page that tells users nothing was found but returns status 200, or redirects missing URLs to the homepage. Search engines flag it because the status code misrepresents the content. Return a real 404 or 410.

Sources

  1. MDN Web Docs: 404 Not Founddeveloper.mozilla.org
  2. RFC 9110 section 15.5.5: 404 Not Foundrfc-editor.org
  3. Google Search Central: Soft 404 errorsdevelopers.google.com
  4. nginx: try_files and proxy_pass URI handlingnginx.org
  5. Next.js: notFound()nextjs.org

Keep going

Browse /search