< HTTP/1.1 404 Not FoundHTTP 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.
- 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
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?
| 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
- The URL is wrong. Case matters on Linux paths (
/Aboutis not/about), as do trailing slashes in some routers. - 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.
- Root path mismatch. nginx
rootvsalias: withlocation /static/ { alias /srv/assets/; }the trailing slashes must match, whereasrootappends the whole URI. - Single-page app without fallback. Deep links like
/dashboard/settings404 on refresh because the server looks for a file. Route unknown paths toindex.html. - Rewrite or proxy path error.
proxy_pass http://app;andproxy_pass http://app/;treat the path differently: a trailing slash replaces the matched location prefix, none passes the URI unchanged. - 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).
- Works with a trailing slash, 404 without (or the reverse). Static hosts map
/docsto/docs/index.htmlonly with directory-style output; check your framework’strailingSlashsetting. - WordPress. Permalinks 404 after migration: Settings > Permalinks > Save, and confirm
mod_rewrite/AllowOverride Allor the nginxtry_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 pageDjango
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
Related
403 vs 404: Forbidden or Not Found
403 Forbidden vs 404 Not Found: when to admit a resource exists, when to hide it, the GitHub private-repo example, and the debugging cost of masking 403 as 404.
400 Bad Request
400 Bad Request means the server could not parse your request. Find which layer sent it, fix bad JSON and oversized cookies or headers, and reproduce with curl.
402 Payment Required
Reserved for future use in digital payment systems. Learn about this experimental status code and modern payment verification alternatives.
405 Method Not Allowed
405 Method Not Allowed means the URL exists but not for that method. Read the Allow header, then fix redirects turning POST into GET, static hosting and WAFs.
406 Not Acceptable
The server cannot produce a response matching the client's Accept headers. Learn about content negotiation and how to handle format mismatches.